@ni/spright-components 6.22.6 → 6.23.0
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/all-components-bundle.js +147 -26
- package/dist/all-components-bundle.js.map +1 -1
- package/dist/all-components-bundle.min.js +4438 -4429
- package/dist/all-components-bundle.min.js.map +1 -1
- package/dist/esm/chat/conversation/models/auto-scroll-manager.d.ts +4 -2
- package/dist/esm/chat/conversation/models/auto-scroll-manager.js +33 -24
- package/dist/esm/chat/conversation/models/auto-scroll-manager.js.map +1 -1
- package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.d.ts +2 -0
- package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.js +6 -0
- package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.js.map +1 -1
- package/package.json +3 -3
|
@@ -30,6 +30,10 @@ export declare class AutoScrollManager implements Subscriber {
|
|
|
30
30
|
connect(): void;
|
|
31
31
|
disconnect(): void;
|
|
32
32
|
handleChange(source: unknown, args: unknown): void;
|
|
33
|
+
/** @internal */
|
|
34
|
+
smoothScrollTo(scrollTop: number): void;
|
|
35
|
+
/** @internal */
|
|
36
|
+
instantScrollTo(scrollTop: number): void;
|
|
33
37
|
private onMessagesChanged;
|
|
34
38
|
private repartition;
|
|
35
39
|
private clearSlotAssignments;
|
|
@@ -44,8 +48,6 @@ export declare class AutoScrollManager implements Subscriber {
|
|
|
44
48
|
private readonly onScroll;
|
|
45
49
|
private getDistanceFromBottom;
|
|
46
50
|
private getMaxScrollTop;
|
|
47
|
-
private smoothScrollTo;
|
|
48
|
-
private instantScrollTo;
|
|
49
51
|
private setScrollAnchorMessage;
|
|
50
52
|
private getLastAnchorMessage;
|
|
51
53
|
private findLatestAnchorIndex;
|
|
@@ -78,8 +78,30 @@ export class AutoScrollManager {
|
|
|
78
78
|
this.onMessagesChanged();
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** @internal */
|
|
82
|
+
smoothScrollTo(scrollTop) {
|
|
83
|
+
const container = this.conversation.messagesContainer;
|
|
84
|
+
if (Math.abs(container.scrollTop - scrollTop) <= 1) {
|
|
85
|
+
// No movement is needed, so `scrollTo` would not emit a scroll event
|
|
86
|
+
// to clear the programmatic guard. Snap to the exact target and
|
|
87
|
+
// leave the guard clear so streamed content keeps being followed.
|
|
88
|
+
this.programmaticScrollTarget = undefined;
|
|
89
|
+
container.scrollTop = scrollTop;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.programmaticScrollTarget = scrollTop;
|
|
93
|
+
container.scrollTo({
|
|
94
|
+
top: scrollTop,
|
|
95
|
+
behavior: 'smooth'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** @internal */
|
|
99
|
+
instantScrollTo(scrollTop) {
|
|
100
|
+
this.conversation.messagesContainer.scrollTop = scrollTop;
|
|
101
|
+
}
|
|
81
102
|
onMessagesChanged() {
|
|
82
103
|
const current = this.getOrderedMessages();
|
|
104
|
+
const isInitialContent = this.previousMessages.length === 0;
|
|
83
105
|
const previousSet = new Set(this.previousMessages);
|
|
84
106
|
const addedMessages = current.filter(message => !previousSet.has(message));
|
|
85
107
|
this.previousMessages = current;
|
|
@@ -88,7 +110,7 @@ export class AutoScrollManager {
|
|
|
88
110
|
return;
|
|
89
111
|
}
|
|
90
112
|
const hasAnchorMessage = addedMessages.some(message => message.messageInternals.anchorOnInsert);
|
|
91
|
-
this.scheduleScrollUpdate(hasAnchorMessage);
|
|
113
|
+
this.scheduleScrollUpdate(hasAnchorMessage, isInitialContent);
|
|
92
114
|
}
|
|
93
115
|
repartition(messages) {
|
|
94
116
|
const anchorIndex = this.findLatestAnchorIndex(messages);
|
|
@@ -104,18 +126,19 @@ export class AutoScrollManager {
|
|
|
104
126
|
message.messageInternals.slot = undefined;
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
|
-
scheduleScrollUpdate(hasAnchorMessage) {
|
|
129
|
+
scheduleScrollUpdate(hasAnchorMessage, isInitialContent) {
|
|
108
130
|
this.pendingAnchorInsert = this.pendingAnchorInsert || hasAnchorMessage;
|
|
109
131
|
if (this.scrollUpdatePending) {
|
|
110
132
|
return;
|
|
111
133
|
}
|
|
134
|
+
const shouldAnimate = !isInitialContent;
|
|
112
135
|
this.scrollUpdatePending = true;
|
|
113
136
|
requestAnimationFrame(() => {
|
|
114
137
|
this.scrollUpdatePending = false;
|
|
115
138
|
const anchorInsert = this.pendingAnchorInsert;
|
|
116
139
|
this.pendingAnchorInsert = false;
|
|
117
140
|
if (anchorInsert) {
|
|
118
|
-
this.anchorToLastInsertedMessage();
|
|
141
|
+
this.anchorToLastInsertedMessage(shouldAnimate);
|
|
119
142
|
}
|
|
120
143
|
else if (this.autoScrollEngaged) {
|
|
121
144
|
this.followContent();
|
|
@@ -126,14 +149,19 @@ export class AutoScrollManager {
|
|
|
126
149
|
* Pins the most recently inserted anchor message near the top of the
|
|
127
150
|
* viewport.
|
|
128
151
|
*/
|
|
129
|
-
anchorToLastInsertedMessage() {
|
|
152
|
+
anchorToLastInsertedMessage(animate) {
|
|
130
153
|
const message = this.getLastAnchorMessage();
|
|
131
154
|
if (message === undefined) {
|
|
132
155
|
return;
|
|
133
156
|
}
|
|
134
157
|
this.setScrollAnchorMessage(message);
|
|
135
158
|
this.autoScrollEngaged = true;
|
|
136
|
-
|
|
159
|
+
if (animate) {
|
|
160
|
+
this.smoothScrollTo(this.getMaxScrollTop());
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.instantScrollTo(this.getMaxScrollTop());
|
|
164
|
+
}
|
|
137
165
|
}
|
|
138
166
|
followContent() {
|
|
139
167
|
this.instantScrollTo(this.getMaxScrollTop());
|
|
@@ -158,25 +186,6 @@ export class AutoScrollManager {
|
|
|
158
186
|
const { scrollHeight, clientHeight } = this.conversation.messagesContainer;
|
|
159
187
|
return Math.max(0, scrollHeight - clientHeight);
|
|
160
188
|
}
|
|
161
|
-
smoothScrollTo(scrollTop) {
|
|
162
|
-
const container = this.conversation.messagesContainer;
|
|
163
|
-
if (Math.abs(container.scrollTop - scrollTop) <= 1) {
|
|
164
|
-
// No movement is needed, so `scrollTo` would not emit a scroll event
|
|
165
|
-
// to clear the programmatic guard. Snap to the exact target and
|
|
166
|
-
// leave the guard clear so streamed content keeps being followed.
|
|
167
|
-
this.programmaticScrollTarget = undefined;
|
|
168
|
-
container.scrollTop = scrollTop;
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
this.programmaticScrollTarget = scrollTop;
|
|
172
|
-
container.scrollTo({
|
|
173
|
-
top: scrollTop,
|
|
174
|
-
behavior: 'smooth'
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
instantScrollTo(scrollTop) {
|
|
178
|
-
this.conversation.messagesContainer.scrollTop = scrollTop;
|
|
179
|
-
}
|
|
180
189
|
setScrollAnchorMessage(message) {
|
|
181
190
|
if (this.scrollAnchorMessage === message) {
|
|
182
191
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-scroll-manager.js","sourceRoot":"","sources":["../../../../../src/chat/conversation/models/auto-scroll-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Notifier,\n Observable,\n observable,\n type Subscriber\n} from '@ni/fast-element';\nimport type { ChatConversation } from '..';\nimport {\n ChatMessageInternals,\n type ChatMessage\n} from '../../message/models/chat-message-internals';\n\n// Distance from the bottom (px) within which the conversation is considered \"at the bottom\".\nconst scrollingPixelThreshold = 10;\n\n// Slot name for messages that precede the current turn's anchor message.\nconst historySlotName = 'history';\n\n/**\n * Manages auto-scroll behavior for the chat conversation:\n * - Splits messages into `default` and `history` slots so the top message of the\n * `default` slot becomes the anchor: it is moved to the top of the viewport and scrolled to on insert\n * - Implements auto scroll when at the bottom of the window as content is added until the user scrolls away\n * @internal\n */\nexport class AutoScrollManager implements Subscriber {\n /**\n * Whether auto-scroll is currently following new content. Set to false when\n * the user scrolls away from the bottom and back to true when they return.\n */\n @observable\n public autoScrollEngaged = true;\n\n /**\n * Whether the anchored region is reserving a viewport of space\n */\n @observable\n public anchorActive = false;\n\n public get isActive(): boolean {\n return this.resizeObserver !== undefined;\n }\n\n private scrollAnchorMessage?: ChatMessage;\n private programmaticScrollTarget?: number;\n private resizeObserver?: ResizeObserver;\n private scrollUpdatePending = false;\n private pendingAnchorInsert = false;\n private previousMessages: ChatMessage[] = [];\n private readonly conversationNotifier: Notifier;\n\n public constructor(private readonly conversation: ChatConversation) {\n this.conversationNotifier = Observable.getNotifier(this.conversation);\n }\n\n public connect(): void {\n this.autoScrollEngaged = true;\n this.previousMessages = this.getOrderedMessages();\n this.conversationNotifier.subscribe(this, 'slottedMessages');\n this.conversationNotifier.subscribe(this, 'slottedHistoryMessages');\n this.conversation.messagesContainer.addEventListener(\n 'scroll',\n this.onScroll,\n { passive: true }\n );\n this.resizeObserver = new ResizeObserver(() => {\n this.onContentSizeChanged();\n });\n // Observe the anchored region for streamed content growth and the scroll\n // viewport so the conversation stays pinned when its height changes.\n this.resizeObserver.observe(this.conversation.anchoredContainer);\n this.resizeObserver.observe(this.conversation.messagesContainer);\n this.repartition(this.previousMessages);\n }\n\n public disconnect(): void {\n this.conversationNotifier.unsubscribe(this, 'slottedMessages');\n this.conversationNotifier.unsubscribe(this, 'slottedHistoryMessages');\n this.conversation.messagesContainer.removeEventListener(\n 'scroll',\n this.onScroll\n );\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.setScrollAnchorMessage(undefined);\n this.clearSlotAssignments();\n this.anchorActive = false;\n this.previousMessages = [];\n }\n\n public handleChange(source: unknown, args: unknown): void {\n if (\n source === this.conversation\n && (args === 'slottedMessages' || args === 'slottedHistoryMessages')\n ) {\n this.onMessagesChanged();\n }\n }\n\n private onMessagesChanged(): void {\n const current = this.getOrderedMessages();\n const previousSet = new Set(this.previousMessages);\n const addedMessages = current.filter(\n message => !previousSet.has(message)\n );\n this.previousMessages = current;\n this.repartition(current);\n if (addedMessages.length === 0) {\n return;\n }\n const hasAnchorMessage = addedMessages.some(\n message => message.messageInternals.anchorOnInsert\n );\n this.scheduleScrollUpdate(hasAnchorMessage);\n }\n\n private repartition(messages: ChatMessage[]): void {\n const anchorIndex = this.findLatestAnchorIndex(messages);\n messages.forEach((message, index) => {\n message.messageInternals.slot = anchorIndex >= 0 && index < anchorIndex\n ? historySlotName\n : undefined;\n });\n this.anchorActive = anchorIndex >= 0;\n }\n\n private clearSlotAssignments(): void {\n for (const message of this.getOrderedMessages()) {\n message.messageInternals.slot = undefined;\n }\n }\n\n private scheduleScrollUpdate(hasAnchorMessage: boolean): void {\n this.pendingAnchorInsert = this.pendingAnchorInsert || hasAnchorMessage;\n if (this.scrollUpdatePending) {\n return;\n }\n this.scrollUpdatePending = true;\n requestAnimationFrame(() => {\n this.scrollUpdatePending = false;\n const anchorInsert = this.pendingAnchorInsert;\n this.pendingAnchorInsert = false;\n if (anchorInsert) {\n this.anchorToLastInsertedMessage();\n } else if (this.autoScrollEngaged) {\n this.followContent();\n }\n });\n }\n\n /**\n * Pins the most recently inserted anchor message near the top of the\n * viewport.\n */\n private anchorToLastInsertedMessage(): void {\n const message = this.getLastAnchorMessage();\n if (message === undefined) {\n return;\n }\n this.setScrollAnchorMessage(message);\n this.autoScrollEngaged = true;\n this.smoothScrollTo(this.getMaxScrollTop());\n }\n\n private followContent(): void {\n this.instantScrollTo(this.getMaxScrollTop());\n }\n\n private onContentSizeChanged(): void {\n // Reacts to streamed content growth and viewport height changes.\n // While a pending or in-progress anchor insert owns positioning, let its\n // smooth scroll settle instead of competing with an instant follow.\n if (\n this.pendingAnchorInsert\n || this.programmaticScrollTarget !== undefined\n ) {\n return;\n }\n if (this.autoScrollEngaged) {\n this.followContent();\n }\n }\n\n private readonly onScroll = (): void => {\n const container = this.conversation.messagesContainer;\n if (this.programmaticScrollTarget !== undefined) {\n // The programmatic scroll always targets the bottom, so treat it as\n // settled once we reach that target or the bottom itself.\n const reachedTarget = Math.abs(container.scrollTop - this.programmaticScrollTarget) <= 1;\n const reachedBottom = this.getDistanceFromBottom() <= scrollingPixelThreshold;\n if (reachedTarget || reachedBottom) {\n this.programmaticScrollTarget = undefined;\n }\n return;\n }\n this.autoScrollEngaged = this.getDistanceFromBottom() <= scrollingPixelThreshold;\n };\n\n private getDistanceFromBottom(): number {\n const { scrollTop, scrollHeight, clientHeight } = this.conversation.messagesContainer;\n return scrollHeight - scrollTop - clientHeight;\n }\n\n private getMaxScrollTop(): number {\n const { scrollHeight, clientHeight } = this.conversation.messagesContainer;\n return Math.max(0, scrollHeight - clientHeight);\n }\n\n private smoothScrollTo(scrollTop: number): void {\n const container = this.conversation.messagesContainer;\n if (Math.abs(container.scrollTop - scrollTop) <= 1) {\n // No movement is needed, so `scrollTo` would not emit a scroll event\n // to clear the programmatic guard. Snap to the exact target and\n // leave the guard clear so streamed content keeps being followed.\n this.programmaticScrollTarget = undefined;\n container.scrollTop = scrollTop;\n return;\n }\n this.programmaticScrollTarget = scrollTop;\n container.scrollTo({\n top: scrollTop,\n behavior: 'smooth'\n });\n }\n\n private instantScrollTo(scrollTop: number): void {\n this.conversation.messagesContainer.scrollTop = scrollTop;\n }\n\n private setScrollAnchorMessage(message?: ChatMessage): void {\n if (this.scrollAnchorMessage === message) {\n return;\n }\n if (this.scrollAnchorMessage !== undefined) {\n this.scrollAnchorMessage.messageInternals.isScrollAnchor = false;\n }\n this.scrollAnchorMessage = message;\n if (message !== undefined) {\n message.messageInternals.isScrollAnchor = true;\n }\n }\n\n private getLastAnchorMessage(): ChatMessage | undefined {\n const messages = this.getOrderedMessages();\n const index = this.findLatestAnchorIndex(messages);\n return index >= 0 ? messages[index] : undefined;\n }\n\n private findLatestAnchorIndex(messages: ChatMessage[]): number {\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i];\n if (message?.messageInternals.anchorOnInsert) {\n return i;\n }\n }\n return -1;\n }\n\n private getOrderedMessages(): ChatMessage[] {\n const messages: ChatMessage[] = [];\n for (const child of Array.from(this.conversation.children)) {\n if (ChatMessageInternals.elementHasMessageInternals(child)) {\n messages.push(child);\n }\n }\n return messages;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auto-scroll-manager.js","sourceRoot":"","sources":["../../../../../src/chat/conversation/models/auto-scroll-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Notifier,\n Observable,\n observable,\n type Subscriber\n} from '@ni/fast-element';\nimport type { ChatConversation } from '..';\nimport {\n ChatMessageInternals,\n type ChatMessage\n} from '../../message/models/chat-message-internals';\n\n// Distance from the bottom (px) within which the conversation is considered \"at the bottom\".\nconst scrollingPixelThreshold = 10;\n\n// Slot name for messages that precede the current turn's anchor message.\nconst historySlotName = 'history';\n\n/**\n * Manages auto-scroll behavior for the chat conversation:\n * - Splits messages into `default` and `history` slots so the top message of the\n * `default` slot becomes the anchor: it is moved to the top of the viewport and scrolled to on insert\n * - Implements auto scroll when at the bottom of the window as content is added until the user scrolls away\n * @internal\n */\nexport class AutoScrollManager implements Subscriber {\n /**\n * Whether auto-scroll is currently following new content. Set to false when\n * the user scrolls away from the bottom and back to true when they return.\n */\n @observable\n public autoScrollEngaged = true;\n\n /**\n * Whether the anchored region is reserving a viewport of space\n */\n @observable\n public anchorActive = false;\n\n public get isActive(): boolean {\n return this.resizeObserver !== undefined;\n }\n\n private scrollAnchorMessage?: ChatMessage;\n private programmaticScrollTarget?: number;\n private resizeObserver?: ResizeObserver;\n private scrollUpdatePending = false;\n private pendingAnchorInsert = false;\n private previousMessages: ChatMessage[] = [];\n private readonly conversationNotifier: Notifier;\n\n public constructor(private readonly conversation: ChatConversation) {\n this.conversationNotifier = Observable.getNotifier(this.conversation);\n }\n\n public connect(): void {\n this.autoScrollEngaged = true;\n this.previousMessages = this.getOrderedMessages();\n this.conversationNotifier.subscribe(this, 'slottedMessages');\n this.conversationNotifier.subscribe(this, 'slottedHistoryMessages');\n this.conversation.messagesContainer.addEventListener(\n 'scroll',\n this.onScroll,\n { passive: true }\n );\n this.resizeObserver = new ResizeObserver(() => {\n this.onContentSizeChanged();\n });\n // Observe the anchored region for streamed content growth and the scroll\n // viewport so the conversation stays pinned when its height changes.\n this.resizeObserver.observe(this.conversation.anchoredContainer);\n this.resizeObserver.observe(this.conversation.messagesContainer);\n this.repartition(this.previousMessages);\n }\n\n public disconnect(): void {\n this.conversationNotifier.unsubscribe(this, 'slottedMessages');\n this.conversationNotifier.unsubscribe(this, 'slottedHistoryMessages');\n this.conversation.messagesContainer.removeEventListener(\n 'scroll',\n this.onScroll\n );\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.setScrollAnchorMessage(undefined);\n this.clearSlotAssignments();\n this.anchorActive = false;\n this.previousMessages = [];\n }\n\n public handleChange(source: unknown, args: unknown): void {\n if (\n source === this.conversation\n && (args === 'slottedMessages' || args === 'slottedHistoryMessages')\n ) {\n this.onMessagesChanged();\n }\n }\n\n /** @internal */\n public smoothScrollTo(scrollTop: number): void {\n const container = this.conversation.messagesContainer;\n if (Math.abs(container.scrollTop - scrollTop) <= 1) {\n // No movement is needed, so `scrollTo` would not emit a scroll event\n // to clear the programmatic guard. Snap to the exact target and\n // leave the guard clear so streamed content keeps being followed.\n this.programmaticScrollTarget = undefined;\n container.scrollTop = scrollTop;\n return;\n }\n this.programmaticScrollTarget = scrollTop;\n container.scrollTo({\n top: scrollTop,\n behavior: 'smooth'\n });\n }\n\n /** @internal */\n public instantScrollTo(scrollTop: number): void {\n this.conversation.messagesContainer.scrollTop = scrollTop;\n }\n\n private onMessagesChanged(): void {\n const current = this.getOrderedMessages();\n const isInitialContent = this.previousMessages.length === 0;\n const previousSet = new Set(this.previousMessages);\n const addedMessages = current.filter(\n message => !previousSet.has(message)\n );\n this.previousMessages = current;\n this.repartition(current);\n if (addedMessages.length === 0) {\n return;\n }\n const hasAnchorMessage = addedMessages.some(\n message => message.messageInternals.anchorOnInsert\n );\n this.scheduleScrollUpdate(hasAnchorMessage, isInitialContent);\n }\n\n private repartition(messages: ChatMessage[]): void {\n const anchorIndex = this.findLatestAnchorIndex(messages);\n messages.forEach((message, index) => {\n message.messageInternals.slot = anchorIndex >= 0 && index < anchorIndex\n ? historySlotName\n : undefined;\n });\n this.anchorActive = anchorIndex >= 0;\n }\n\n private clearSlotAssignments(): void {\n for (const message of this.getOrderedMessages()) {\n message.messageInternals.slot = undefined;\n }\n }\n\n private scheduleScrollUpdate(\n hasAnchorMessage: boolean,\n isInitialContent: boolean\n ): void {\n this.pendingAnchorInsert = this.pendingAnchorInsert || hasAnchorMessage;\n if (this.scrollUpdatePending) {\n return;\n }\n const shouldAnimate = !isInitialContent;\n this.scrollUpdatePending = true;\n requestAnimationFrame(() => {\n this.scrollUpdatePending = false;\n const anchorInsert = this.pendingAnchorInsert;\n this.pendingAnchorInsert = false;\n if (anchorInsert) {\n this.anchorToLastInsertedMessage(shouldAnimate);\n } else if (this.autoScrollEngaged) {\n this.followContent();\n }\n });\n }\n\n /**\n * Pins the most recently inserted anchor message near the top of the\n * viewport.\n */\n private anchorToLastInsertedMessage(animate: boolean): void {\n const message = this.getLastAnchorMessage();\n if (message === undefined) {\n return;\n }\n this.setScrollAnchorMessage(message);\n this.autoScrollEngaged = true;\n if (animate) {\n this.smoothScrollTo(this.getMaxScrollTop());\n } else {\n this.instantScrollTo(this.getMaxScrollTop());\n }\n }\n\n private followContent(): void {\n this.instantScrollTo(this.getMaxScrollTop());\n }\n\n private onContentSizeChanged(): void {\n // Reacts to streamed content growth and viewport height changes.\n // While a pending or in-progress anchor insert owns positioning, let its\n // smooth scroll settle instead of competing with an instant follow.\n if (\n this.pendingAnchorInsert\n || this.programmaticScrollTarget !== undefined\n ) {\n return;\n }\n if (this.autoScrollEngaged) {\n this.followContent();\n }\n }\n\n private readonly onScroll = (): void => {\n const container = this.conversation.messagesContainer;\n if (this.programmaticScrollTarget !== undefined) {\n // The programmatic scroll always targets the bottom, so treat it as\n // settled once we reach that target or the bottom itself.\n const reachedTarget = Math.abs(container.scrollTop - this.programmaticScrollTarget) <= 1;\n const reachedBottom = this.getDistanceFromBottom() <= scrollingPixelThreshold;\n if (reachedTarget || reachedBottom) {\n this.programmaticScrollTarget = undefined;\n }\n return;\n }\n this.autoScrollEngaged = this.getDistanceFromBottom() <= scrollingPixelThreshold;\n };\n\n private getDistanceFromBottom(): number {\n const { scrollTop, scrollHeight, clientHeight } = this.conversation.messagesContainer;\n return scrollHeight - scrollTop - clientHeight;\n }\n\n private getMaxScrollTop(): number {\n const { scrollHeight, clientHeight } = this.conversation.messagesContainer;\n return Math.max(0, scrollHeight - clientHeight);\n }\n\n private setScrollAnchorMessage(message?: ChatMessage): void {\n if (this.scrollAnchorMessage === message) {\n return;\n }\n if (this.scrollAnchorMessage !== undefined) {\n this.scrollAnchorMessage.messageInternals.isScrollAnchor = false;\n }\n this.scrollAnchorMessage = message;\n if (message !== undefined) {\n message.messageInternals.isScrollAnchor = true;\n }\n }\n\n private getLastAnchorMessage(): ChatMessage | undefined {\n const messages = this.getOrderedMessages();\n const index = this.findLatestAnchorIndex(messages);\n return index >= 0 ? messages[index] : undefined;\n }\n\n private findLatestAnchorIndex(messages: ChatMessage[]): number {\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i];\n if (message?.messageInternals.anchorOnInsert) {\n return i;\n }\n }\n return -1;\n }\n\n private getOrderedMessages(): ChatMessage[] {\n const messages: ChatMessage[] = [];\n for (const child of Array.from(this.conversation.children)) {\n if (ChatMessageInternals.elementHasMessageInternals(child)) {\n messages.push(child);\n }\n }\n return messages;\n }\n}\n"]}
|
|
@@ -31,6 +31,8 @@ export declare class ChatConversationPageObject {
|
|
|
31
31
|
getProgrammaticScrollTarget(): number | undefined;
|
|
32
32
|
setProgrammaticScrollTarget(target: number): void;
|
|
33
33
|
dispatchScroll(): void;
|
|
34
|
+
spyOnSmoothScrollTo(): jasmine.Spy;
|
|
35
|
+
spyOnInstantScrollTo(): jasmine.Spy;
|
|
34
36
|
private get autoScrollManagerInternals();
|
|
35
37
|
appendOutboundMessage(text: string): Promise<void>;
|
|
36
38
|
appendInboundMessage(text: string): Promise<void>;
|
|
@@ -109,6 +109,12 @@ export class ChatConversationPageObject {
|
|
|
109
109
|
dispatchScroll() {
|
|
110
110
|
this.getScrollContainer().dispatchEvent(new Event('scroll'));
|
|
111
111
|
}
|
|
112
|
+
spyOnSmoothScrollTo() {
|
|
113
|
+
return spyOn(this.element.autoScrollManager, 'smoothScrollTo').and.callThrough();
|
|
114
|
+
}
|
|
115
|
+
spyOnInstantScrollTo() {
|
|
116
|
+
return spyOn(this.element.autoScrollManager, 'instantScrollTo').and.callThrough();
|
|
117
|
+
}
|
|
112
118
|
get autoScrollManagerInternals() {
|
|
113
119
|
return this.element.autoScrollManager;
|
|
114
120
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-conversation.pageobject.js","sourceRoot":"","sources":["../../../../../src/chat/conversation/testing/chat-conversation.pageobject.ts"],"names":[],"mappings":"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","sourcesContent":["import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';\nimport type { ChatConversation } from '..';\nimport { chatMessageInboundTag } from '../../message/inbound';\nimport { chatMessageOutboundTag } from '../../message/outbound';\nimport { chatMessageSystemTag } from '../../message/system';\n\n/**\n * Page object for the `spright-chat-conversation`\n */\nexport class ChatConversationPageObject {\n public constructor(protected readonly element: ChatConversation) {}\n\n // --- slots ---\n\n public hasDefaultSlot(): boolean {\n return this.queryShadow('.messages slot:not([name])') !== undefined;\n }\n\n public hasNamedSlot(name: string): boolean {\n return this.queryShadow(`slot[name=\"${name}\"]`) !== undefined;\n }\n\n public getNamedSlotAssignedElementCount(name: string): number {\n const slot = this.queryShadow(`slot[name=\"${name}\"]`) as\n | HTMLSlotElement\n | undefined;\n return slot?.assignedElements().length ?? 0;\n }\n\n // --- messages ---\n\n public getMessageCount(): number {\n return this.getMessages().length;\n }\n\n public getMessageTextByIndex(index: number): string {\n const message = this.getMessages()[index];\n if (message === undefined) {\n throw new Error(`No message at index ${index}`);\n }\n return message.textContent?.trim() ?? '';\n }\n\n public getLastMessageText(): string {\n return this.getMessageTextByIndex(this.getMessageCount() - 1);\n }\n\n public isMessageOutboundByIndex(index: number): boolean {\n const message = this.getMessages()[index];\n return message?.tagName.toLowerCase() === chatMessageOutboundTag;\n }\n\n // --- scroll metrics ---\n\n public getScrollTop(): number {\n return this.getScrollContainer().scrollTop;\n }\n\n public getScrollHeight(): number {\n return this.getScrollContainer().scrollHeight;\n }\n\n public getClientHeight(): number {\n return this.getScrollContainer().clientHeight;\n }\n\n public isAnchorRegionReserved(): boolean {\n return this.element.autoScrollManager.anchorActive;\n }\n\n public getHistoryRegionMessageCount(): number {\n return this.getNamedSlotAssignedElementCount('history');\n }\n\n public getAnchoredRegionMessageCount(): number {\n const slot = this.queryShadow('.messages-anchored slot') as\n | HTMLSlotElement\n | undefined;\n return slot?.assignedElements().length ?? 0;\n }\n\n public getMessageSlotByIndex(index: number): string {\n const message = this.getMessages()[index];\n return message?.getAttribute('slot') ?? '';\n }\n\n public getAnchoredRegionHeight(): number {\n const region = this.queryShadow('.messages-anchored') as\n | HTMLElement\n | undefined;\n return region?.clientHeight ?? 0;\n }\n\n public getMessageViewportTop(index: number): number {\n const message = this.getMessages()[index];\n if (message === undefined) {\n throw new Error(`No message at index ${index}`);\n }\n const container = this.getScrollContainer();\n return (\n message.getBoundingClientRect().top\n - container.getBoundingClientRect().top\n );\n }\n\n public isMessagePinnedNearTop(index: number, tolerance = 48): boolean {\n return Math.abs(this.getMessageViewportTop(index)) <= tolerance;\n }\n\n public getDistanceFromBottom(): number {\n const container = this.getScrollContainer();\n return (\n container.scrollHeight - container.scrollTop - container.clientHeight\n );\n }\n\n public isScrolledToBottom(threshold = 10): boolean {\n return this.getDistanceFromBottom() <= threshold;\n }\n\n // --- auto-scroll state ---\n\n public isAutoScrollEnabled(): boolean {\n return this.element.autoScroll;\n }\n\n public isAutoScrollConnected(): boolean {\n return this.element.autoScrollManager.isActive;\n }\n\n public isAutoScrollEngaged(): boolean {\n return this.element.autoScrollManager.autoScrollEngaged;\n }\n\n public isMessageScrollAnchorByIndex(index: number): boolean {\n const message = this.getMessages()[index] as unknown as\n | { messageInternals?: { isScrollAnchor: boolean } }\n | undefined;\n return message?.messageInternals?.isScrollAnchor ?? false;\n }\n\n // --- programmatic scroll handling (test seams) ---\n\n public getProgrammaticScrollTarget(): number | undefined {\n return this.autoScrollManagerInternals.programmaticScrollTarget;\n }\n\n public setProgrammaticScrollTarget(target: number): void {\n this.autoScrollManagerInternals.programmaticScrollTarget = target;\n }\n\n public dispatchScroll(): void {\n this.getScrollContainer().dispatchEvent(new Event('scroll'));\n }\n\n private get autoScrollManagerInternals(): {\n programmaticScrollTarget?: number\n } {\n return this.element.autoScrollManager as unknown as {\n programmaticScrollTarget?: number\n };\n }\n\n // --- actions ---\n\n public async appendOutboundMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageOutboundTag, text);\n }\n\n public async appendInboundMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageInboundTag, text);\n }\n\n public async appendSystemMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageSystemTag, text);\n }\n\n public async appendTextToLastMessage(text: string): Promise<void> {\n const messages = this.getMessages();\n const lastMessage = messages[messages.length - 1];\n if (lastMessage === undefined) {\n throw new Error('No message to append text to');\n }\n lastMessage.textContent = `${lastMessage.textContent ?? ''}${text}`;\n await this.waitForScrollUpdateAsync();\n }\n\n public async setViewportHeight(height: number): Promise<void> {\n this.element.style.height = `${height}px`;\n await this.waitForScrollUpdateAsync();\n }\n\n public async setAutoScrollEnabled(enabled: boolean): Promise<void> {\n this.element.autoScroll = enabled;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollToOffset(offset: number): Promise<void> {\n this.getScrollContainer().scrollTop = offset;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollByOffset(delta: number): Promise<void> {\n this.getScrollContainer().scrollTop += delta;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollToBottom(): Promise<void> {\n await this.scrollToOffset(this.getScrollHeight());\n }\n\n /**\n * Waits for FAST updates plus two animation frames so reactions driven by\n * `requestAnimationFrame` and `ResizeObserver` (used by scroll management)\n * have settled.\n */\n public async waitForScrollUpdateAsync(): Promise<void> {\n await waitForUpdatesAsync();\n await this.nextAnimationFrameAsync();\n await this.nextAnimationFrameAsync();\n await waitForUpdatesAsync();\n }\n\n private async nextAnimationFrameAsync(): Promise<void> {\n await new Promise<void>(resolve => {\n requestAnimationFrame(() => resolve());\n });\n }\n\n private async appendMessage(tag: string, text: string): Promise<void> {\n const message = document.createElement(tag);\n message.textContent = text;\n this.element.appendChild(message);\n await this.waitForScrollUpdateAsync();\n }\n\n private getMessages(): Element[] {\n const history = this.queryShadow('slot[name=\"history\"]') as\n | HTMLSlotElement\n | undefined;\n const anchored = this.queryShadow('.messages-anchored slot') as\n | HTMLSlotElement\n | undefined;\n return [\n ...(history?.assignedElements({ flatten: true }) ?? []),\n ...(anchored?.assignedElements({ flatten: true }) ?? [])\n ];\n }\n\n private getScrollContainer(): HTMLElement {\n const container = this.queryShadow('.messages') as\n | HTMLElement\n | undefined;\n if (container === undefined) {\n throw new Error('Scroll container not found');\n }\n return container;\n }\n\n private queryShadow(selector: string): Element | undefined {\n return this.element.shadowRoot?.querySelector(selector) ?? undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chat-conversation.pageobject.js","sourceRoot":"","sources":["../../../../../src/chat/conversation/testing/chat-conversation.pageobject.ts"],"names":[],"mappings":"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","sourcesContent":["import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';\nimport type { ChatConversation } from '..';\nimport { chatMessageInboundTag } from '../../message/inbound';\nimport { chatMessageOutboundTag } from '../../message/outbound';\nimport { chatMessageSystemTag } from '../../message/system';\n\n/**\n * Page object for the `spright-chat-conversation`\n */\nexport class ChatConversationPageObject {\n public constructor(protected readonly element: ChatConversation) {}\n\n // --- slots ---\n\n public hasDefaultSlot(): boolean {\n return this.queryShadow('.messages slot:not([name])') !== undefined;\n }\n\n public hasNamedSlot(name: string): boolean {\n return this.queryShadow(`slot[name=\"${name}\"]`) !== undefined;\n }\n\n public getNamedSlotAssignedElementCount(name: string): number {\n const slot = this.queryShadow(`slot[name=\"${name}\"]`) as\n | HTMLSlotElement\n | undefined;\n return slot?.assignedElements().length ?? 0;\n }\n\n // --- messages ---\n\n public getMessageCount(): number {\n return this.getMessages().length;\n }\n\n public getMessageTextByIndex(index: number): string {\n const message = this.getMessages()[index];\n if (message === undefined) {\n throw new Error(`No message at index ${index}`);\n }\n return message.textContent?.trim() ?? '';\n }\n\n public getLastMessageText(): string {\n return this.getMessageTextByIndex(this.getMessageCount() - 1);\n }\n\n public isMessageOutboundByIndex(index: number): boolean {\n const message = this.getMessages()[index];\n return message?.tagName.toLowerCase() === chatMessageOutboundTag;\n }\n\n // --- scroll metrics ---\n\n public getScrollTop(): number {\n return this.getScrollContainer().scrollTop;\n }\n\n public getScrollHeight(): number {\n return this.getScrollContainer().scrollHeight;\n }\n\n public getClientHeight(): number {\n return this.getScrollContainer().clientHeight;\n }\n\n public isAnchorRegionReserved(): boolean {\n return this.element.autoScrollManager.anchorActive;\n }\n\n public getHistoryRegionMessageCount(): number {\n return this.getNamedSlotAssignedElementCount('history');\n }\n\n public getAnchoredRegionMessageCount(): number {\n const slot = this.queryShadow('.messages-anchored slot') as\n | HTMLSlotElement\n | undefined;\n return slot?.assignedElements().length ?? 0;\n }\n\n public getMessageSlotByIndex(index: number): string {\n const message = this.getMessages()[index];\n return message?.getAttribute('slot') ?? '';\n }\n\n public getAnchoredRegionHeight(): number {\n const region = this.queryShadow('.messages-anchored') as\n | HTMLElement\n | undefined;\n return region?.clientHeight ?? 0;\n }\n\n public getMessageViewportTop(index: number): number {\n const message = this.getMessages()[index];\n if (message === undefined) {\n throw new Error(`No message at index ${index}`);\n }\n const container = this.getScrollContainer();\n return (\n message.getBoundingClientRect().top\n - container.getBoundingClientRect().top\n );\n }\n\n public isMessagePinnedNearTop(index: number, tolerance = 48): boolean {\n return Math.abs(this.getMessageViewportTop(index)) <= tolerance;\n }\n\n public getDistanceFromBottom(): number {\n const container = this.getScrollContainer();\n return (\n container.scrollHeight - container.scrollTop - container.clientHeight\n );\n }\n\n public isScrolledToBottom(threshold = 10): boolean {\n return this.getDistanceFromBottom() <= threshold;\n }\n\n // --- auto-scroll state ---\n\n public isAutoScrollEnabled(): boolean {\n return this.element.autoScroll;\n }\n\n public isAutoScrollConnected(): boolean {\n return this.element.autoScrollManager.isActive;\n }\n\n public isAutoScrollEngaged(): boolean {\n return this.element.autoScrollManager.autoScrollEngaged;\n }\n\n public isMessageScrollAnchorByIndex(index: number): boolean {\n const message = this.getMessages()[index] as unknown as\n | { messageInternals?: { isScrollAnchor: boolean } }\n | undefined;\n return message?.messageInternals?.isScrollAnchor ?? false;\n }\n\n // --- programmatic scroll handling (test seams) ---\n\n public getProgrammaticScrollTarget(): number | undefined {\n return this.autoScrollManagerInternals.programmaticScrollTarget;\n }\n\n public setProgrammaticScrollTarget(target: number): void {\n this.autoScrollManagerInternals.programmaticScrollTarget = target;\n }\n\n public dispatchScroll(): void {\n this.getScrollContainer().dispatchEvent(new Event('scroll'));\n }\n\n public spyOnSmoothScrollTo(): jasmine.Spy {\n return spyOn(this.element.autoScrollManager, 'smoothScrollTo').and.callThrough();\n }\n\n public spyOnInstantScrollTo(): jasmine.Spy {\n return spyOn(this.element.autoScrollManager, 'instantScrollTo').and.callThrough();\n }\n\n private get autoScrollManagerInternals(): {\n programmaticScrollTarget?: number\n } {\n return this.element.autoScrollManager as unknown as {\n programmaticScrollTarget?: number\n };\n }\n\n // --- actions ---\n\n public async appendOutboundMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageOutboundTag, text);\n }\n\n public async appendInboundMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageInboundTag, text);\n }\n\n public async appendSystemMessage(text: string): Promise<void> {\n await this.appendMessage(chatMessageSystemTag, text);\n }\n\n public async appendTextToLastMessage(text: string): Promise<void> {\n const messages = this.getMessages();\n const lastMessage = messages[messages.length - 1];\n if (lastMessage === undefined) {\n throw new Error('No message to append text to');\n }\n lastMessage.textContent = `${lastMessage.textContent ?? ''}${text}`;\n await this.waitForScrollUpdateAsync();\n }\n\n public async setViewportHeight(height: number): Promise<void> {\n this.element.style.height = `${height}px`;\n await this.waitForScrollUpdateAsync();\n }\n\n public async setAutoScrollEnabled(enabled: boolean): Promise<void> {\n this.element.autoScroll = enabled;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollToOffset(offset: number): Promise<void> {\n this.getScrollContainer().scrollTop = offset;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollByOffset(delta: number): Promise<void> {\n this.getScrollContainer().scrollTop += delta;\n await this.waitForScrollUpdateAsync();\n }\n\n public async scrollToBottom(): Promise<void> {\n await this.scrollToOffset(this.getScrollHeight());\n }\n\n /**\n * Waits for FAST updates plus two animation frames so reactions driven by\n * `requestAnimationFrame` and `ResizeObserver` (used by scroll management)\n * have settled.\n */\n public async waitForScrollUpdateAsync(): Promise<void> {\n await waitForUpdatesAsync();\n await this.nextAnimationFrameAsync();\n await this.nextAnimationFrameAsync();\n await waitForUpdatesAsync();\n }\n\n private async nextAnimationFrameAsync(): Promise<void> {\n await new Promise<void>(resolve => {\n requestAnimationFrame(() => resolve());\n });\n }\n\n private async appendMessage(tag: string, text: string): Promise<void> {\n const message = document.createElement(tag);\n message.textContent = text;\n this.element.appendChild(message);\n await this.waitForScrollUpdateAsync();\n }\n\n private getMessages(): Element[] {\n const history = this.queryShadow('slot[name=\"history\"]') as\n | HTMLSlotElement\n | undefined;\n const anchored = this.queryShadow('.messages-anchored slot') as\n | HTMLSlotElement\n | undefined;\n return [\n ...(history?.assignedElements({ flatten: true }) ?? []),\n ...(anchored?.assignedElements({ flatten: true }) ?? [])\n ];\n }\n\n private getScrollContainer(): HTMLElement {\n const container = this.queryShadow('.messages') as\n | HTMLElement\n | undefined;\n if (container === undefined) {\n throw new Error('Scroll container not found');\n }\n return container;\n }\n\n private queryShadow(selector: string): Element | undefined {\n return this.element.shadowRoot?.querySelector(selector) ?? undefined;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ni/spright-components",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.23.0",
|
|
4
4
|
"description": "NI Spright Components",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "npm run build:components && npm run build:cem",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"@ni/fast-element": "^10.2.0",
|
|
44
44
|
"@ni/fast-foundation": "^10.3.0",
|
|
45
45
|
"@ni/fast-web-utilities": "^10.1.0",
|
|
46
|
-
"@ni/nimble-components": "^35.
|
|
47
|
-
"@ni/nimble-tokens": "^8.
|
|
46
|
+
"@ni/nimble-components": "^35.15.0",
|
|
47
|
+
"@ni/nimble-tokens": "^8.20.0",
|
|
48
48
|
"tslib": "^2.2.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|