@ni/spright-components 6.22.5 → 6.22.7

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.
@@ -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
- this.smoothScrollTo(this.getMaxScrollTop());
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":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAE3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;GAEG;AACH,MAAM,OAAO,0BAA0B;IACnC,YAAsC,OAAyB;QAAzB,YAAO,GAAP,OAAO,CAAkB;IAAG,CAAC;IAEnE,gBAAgB;IAET,cAAc;QACjB,OAAO,IAAI,CAAC,WAAW,CAAC,4BAA4B,CAAC,KAAK,SAAS,CAAC;IACxE,CAAC;IAEM,YAAY,CAAC,IAAY;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,IAAI,IAAI,CAAC,KAAK,SAAS,CAAC;IAClE,CAAC;IAEM,gCAAgC,CAAC,IAAY;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,cAAc,IAAI,IAAI,CAErC,CAAC;QAChB,OAAO,IAAI,EAAE,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,mBAAmB;IAEZ,eAAe;QAClB,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;IACrC,CAAC;IAEM,qBAAqB,CAAC,KAAa;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAEM,kBAAkB;QACrB,OAAO,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,CAAC;IAClE,CAAC;IAEM,wBAAwB,CAAC,KAAa;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,OAAO,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,sBAAsB,CAAC;IACrE,CAAC;IAED,yBAAyB;IAElB,YAAY;QACf,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC,SAAS,CAAC;IAC/C,CAAC;IAEM,eAAe;QAClB,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC,YAAY,CAAC;IAClD,CAAC;IAEM,eAAe;QAClB,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC,YAAY,CAAC;IAClD,CAAC;IAEM,sBAAsB;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,YAAY,CAAC;IACvD,CAAC;IAEM,4BAA4B;QAC/B,OAAO,IAAI,CAAC,gCAAgC,CAAC,SAAS,CAAC,CAAC;IAC5D,CAAC;IAEM,6BAA6B;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAExC,CAAC;QAChB,OAAO,IAAI,EAAE,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC;IAChD,CAAC;IAEM,qBAAqB,CAAC,KAAa;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC/C,CAAC;IAEM,uBAAuB;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAErC,CAAC;QAChB,OAAO,MAAM,EAAE,YAAY,IAAI,CAAC,CAAC;IACrC,CAAC;IAEM,qBAAqB,CAAC,KAAa;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC5C,OAAO,CACH,OAAO,CAAC,qBAAqB,EAAE,CAAC,GAAG;cACjC,SAAS,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAC1C,CAAC;IACN,CAAC;IAEM,sBAAsB,CAAC,KAAa,EAAE,SAAS,GAAG,EAAE;QACvD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,IAAI,SAAS,CAAC;IACpE,CAAC;IAEM,qBAAqB;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC5C,OAAO,CACH,SAAS,CAAC,YAAY,GAAG,SAAS,CAAC,SAAS,GAAG,SAAS,CAAC,YAAY,CACxE,CAAC;IACN,CAAC;IAEM,kBAAkB,CAAC,SAAS,GAAG,EAAE;QACpC,OAAO,IAAI,CAAC,qBAAqB,EAAE,IAAI,SAAS,CAAC;IACrD,CAAC;IAED,4BAA4B;IAErB,mBAAmB;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;IACnC,CAAC;IAEM,qBAAqB;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC;IACnD,CAAC;IAEM,mBAAmB;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,iBAAiB,CAAC;IAC5D,CAAC;IAEM,4BAA4B,CAAC,KAAa;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAEzB,CAAC;QAChB,OAAO,OAAO,EAAE,gBAAgB,EAAE,cAAc,IAAI,KAAK,CAAC;IAC9D,CAAC;IAED,oDAAoD;IAE7C,2BAA2B;QAC9B,OAAO,IAAI,CAAC,0BAA0B,CAAC,wBAAwB,CAAC;IACpE,CAAC;IAEM,2BAA2B,CAAC,MAAc;QAC7C,IAAI,CAAC,0BAA0B,CAAC,wBAAwB,GAAG,MAAM,CAAC;IACtE,CAAC;IAEM,cAAc;QACjB,IAAI,CAAC,kBAAkB,EAAE,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;IAEM,mBAAmB;QACtB,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IACrF,CAAC;IAEM,oBAAoB;QACvB,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IACtF,CAAC;IAED,IAAY,0BAA0B;QAGlC,OAAO,IAAI,CAAC,OAAO,CAAC,iBAEnB,CAAC;IACN,CAAC;IAED,kBAAkB;IAEX,KAAK,CAAC,qBAAqB,CAAC,IAAY;QAC3C,MAAM,IAAI,CAAC,aAAa,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;IAEM,KAAK,CAAC,oBAAoB,CAAC,IAAY;QAC1C,MAAM,IAAI,CAAC,aAAa,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAEM,KAAK,CAAC,mBAAmB,CAAC,IAAY;QACzC,MAAM,IAAI,CAAC,aAAa,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAEM,KAAK,CAAC,uBAAuB,CAAC,IAAY;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QACpD,CAAC;QACD,WAAW,CAAC,WAAW,GAAG,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;QACpE,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEM,KAAK,CAAC,iBAAiB,CAAC,MAAc;QACzC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC;QAC1C,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEM,KAAK,CAAC,oBAAoB,CAAC,OAAgB;QAC9C,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC;QAClC,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEM,KAAK,CAAC,cAAc,CAAC,MAAc;QACtC,IAAI,CAAC,kBAAkB,EAAE,CAAC,SAAS,GAAG,MAAM,CAAC;QAC7C,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEM,KAAK,CAAC,cAAc,CAAC,KAAa;QACrC,IAAI,CAAC,kBAAkB,EAAE,CAAC,SAAS,IAAI,KAAK,CAAC;QAC7C,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEM,KAAK,CAAC,cAAc;QACvB,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;IACtD,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,wBAAwB;QACjC,MAAM,mBAAmB,EAAE,CAAC;QAC5B,MAAM,IAAI,CAAC,uBAAuB,EAAE,CAAC;QACrC,MAAM,IAAI,CAAC,uBAAuB,EAAE,CAAC;QACrC,MAAM,mBAAmB,EAAE,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,uBAAuB;QACjC,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;YAC9B,qBAAqB,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAW,EAAE,IAAY;QACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAC5C,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;IAC1C,CAAC;IAEO,WAAW;QACf,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,sBAAsB,CAExC,CAAC;QAChB,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAE5C,CAAC;QAChB,OAAO;YACH,GAAG,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACvD,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;SAC3D,CAAC;IACN,CAAC;IAEO,kBAAkB;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,WAAW,CAE/B,CAAC;QAChB,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAEO,WAAW,CAAC,QAAgB;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC;IACzE,CAAC;CACJ","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.22.5",
3
+ "version": "6.22.7",
4
4
  "description": "NI Spright Components",
5
5
  "scripts": {
6
6
  "build": "npm run build:components && npm run build:cem",
@@ -43,7 +43,7 @@
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.14.0",
46
+ "@ni/nimble-components": "^35.14.1",
47
47
  "@ni/nimble-tokens": "^8.19.2",
48
48
  "tslib": "^2.2.0"
49
49
  },
@@ -52,7 +52,7 @@
52
52
  "@custom-elements-manifest/to-markdown": "^0.1.0",
53
53
  "@ni-private/eslint-config-nimble": "*",
54
54
  "@ni-private/jasmine-extensions": "^0.0.1",
55
- "@ni/jasmine-parameterized": "^1.0.12",
55
+ "@ni/jasmine-parameterized": "^1.0.13",
56
56
  "@rollup/plugin-commonjs": "^29.0.0",
57
57
  "@rollup/plugin-node-resolve": "^16.0.0",
58
58
  "@rollup/plugin-replace": "^6.0.0",