@ni/spright-components 6.20.4 → 6.21.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.
Files changed (35) hide show
  1. package/dist/all-components-bundle.js +342 -2
  2. package/dist/all-components-bundle.js.map +1 -1
  3. package/dist/all-components-bundle.min.js +2719 -2676
  4. package/dist/all-components-bundle.min.js.map +1 -1
  5. package/dist/custom-elements.json +51 -0
  6. package/dist/custom-elements.md +10 -6
  7. package/dist/esm/chat/conversation/index.d.ts +22 -0
  8. package/dist/esm/chat/conversation/index.js +47 -0
  9. package/dist/esm/chat/conversation/index.js.map +1 -1
  10. package/dist/esm/chat/conversation/models/auto-scroll-manager.d.ts +53 -0
  11. package/dist/esm/chat/conversation/models/auto-scroll-manager.js +222 -0
  12. package/dist/esm/chat/conversation/models/auto-scroll-manager.js.map +1 -0
  13. package/dist/esm/chat/conversation/styles.js +18 -1
  14. package/dist/esm/chat/conversation/styles.js.map +1 -1
  15. package/dist/esm/chat/conversation/template.js +9 -2
  16. package/dist/esm/chat/conversation/template.js.map +1 -1
  17. package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.d.ts +55 -0
  18. package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.js +194 -0
  19. package/dist/esm/chat/conversation/testing/chat-conversation.pageobject.js.map +1 -0
  20. package/dist/esm/chat/message/inbound/index.d.ts +3 -0
  21. package/dist/esm/chat/message/inbound/index.js +3 -0
  22. package/dist/esm/chat/message/inbound/index.js.map +1 -1
  23. package/dist/esm/chat/message/models/chat-message-internals.d.ts +38 -0
  24. package/dist/esm/chat/message/models/chat-message-internals.js +39 -0
  25. package/dist/esm/chat/message/models/chat-message-internals.js.map +1 -0
  26. package/dist/esm/chat/message/outbound/index.d.ts +3 -0
  27. package/dist/esm/chat/message/outbound/index.js +8 -0
  28. package/dist/esm/chat/message/outbound/index.js.map +1 -1
  29. package/dist/esm/chat/message/system/index.d.ts +3 -0
  30. package/dist/esm/chat/message/system/index.js +6 -0
  31. package/dist/esm/chat/message/system/index.js.map +1 -1
  32. package/dist/esm/chat/message/testing/chat-message.pageobject.d.ts +18 -0
  33. package/dist/esm/chat/message/testing/chat-message.pageobject.js +27 -0
  34. package/dist/esm/chat/message/testing/chat-message.pageobject.js.map +1 -0
  35. package/package.json +1 -1
@@ -100078,6 +100078,7 @@ focus outline in that case.
100078
100078
  ${display('flex')}
100079
100079
 
100080
100080
  :host {
100081
+ height: 480px;
100081
100082
  flex-direction: column;
100082
100083
  background: ${applicationBackgroundColor};
100083
100084
  }
@@ -100125,12 +100126,28 @@ focus outline in that case.
100125
100126
  flex: 1;
100126
100127
  display: flex;
100127
100128
  flex-direction: column;
100128
- justify-content: flex-start;
100129
100129
  row-gap: 32px;
100130
100130
  padding: ${mediumPadding} ${standardPadding} ${mediumPadding}
100131
100131
  ${standardPadding};
100132
100132
  background: ${sectionBackgroundImage};
100133
100133
  overflow-y: auto;
100134
+ overflow-anchor: none;
100135
+ }
100136
+
100137
+ .messages-history,
100138
+ .messages-anchored {
100139
+ flex: none;
100140
+ display: flex;
100141
+ flex-direction: column;
100142
+ row-gap: 32px;
100143
+ }
100144
+
100145
+ .messages-history.region-empty {
100146
+ display: none;
100147
+ }
100148
+
100149
+ .messages-anchored.anchor-active {
100150
+ min-height: 100%;
100134
100151
  }
100135
100152
 
100136
100153
  :host([appearance='overlay']) .messages {
@@ -100154,7 +100171,14 @@ focus outline in that case.
100154
100171
  <div class="start ${x => (x.startEmpty ? 'start-empty' : '')}">
100155
100172
  <slot name="start" ${slotted({ property: 'slottedStartElements' })}></slot>
100156
100173
  </div>
100157
- <div class="messages"><slot></slot></div>
100174
+ <div class="messages" ${ref('messagesContainer')}>
100175
+ <div class="messages-history ${x => (x.historyEmpty ? 'region-empty' : '')}">
100176
+ <slot name="history" ${slotted({ property: 'slottedHistoryMessages' })}></slot>
100177
+ </div>
100178
+ <div class="messages-anchored ${x => (x.autoScrollManager.anchorActive ? 'anchor-active' : '')}" ${ref('anchoredContainer')}>
100179
+ <slot ${slotted({ property: 'slottedMessages' })}></slot>
100180
+ </div>
100181
+ </div>
100158
100182
  <div class="input ${x => (x.inputEmpty ? 'input-empty' : '')}">
100159
100183
  <slot name="input" ${slotted({ property: 'slottedInputElements' })}>
100160
100184
  </slot>
@@ -100171,6 +100195,262 @@ focus outline in that case.
100171
100195
  const ChatConversationAppearance = {
100172
100196
  default: undefined};
100173
100197
 
100198
+ /**
100199
+ * Internal state for a chat message
100200
+ * @internal
100201
+ */
100202
+ class ChatMessageInternals {
100203
+ constructor(host, options) {
100204
+ /**
100205
+ * True when this message is the one the conversation anchors to while
100206
+ * auto-scrolling.
100207
+ */
100208
+ this.isScrollAnchor = false;
100209
+ this.host = host;
100210
+ this.anchorOnInsert = options?.anchorOnInsert ?? false;
100211
+ }
100212
+ get slot() {
100213
+ return this.slotName;
100214
+ }
100215
+ set slot(value) {
100216
+ if (value === this.slotName) {
100217
+ return;
100218
+ }
100219
+ this.slotName = value;
100220
+ if (value === undefined) {
100221
+ this.host.removeAttribute('slot');
100222
+ }
100223
+ else {
100224
+ this.host.setAttribute('slot', value);
100225
+ }
100226
+ }
100227
+ static elementHasMessageInternals(element) {
100228
+ return element.messageInternals instanceof ChatMessageInternals;
100229
+ }
100230
+ }
100231
+ __decorate([
100232
+ observable
100233
+ ], ChatMessageInternals.prototype, "isScrollAnchor", void 0);
100234
+
100235
+ // Distance from the bottom (px) within which the conversation is considered "at the bottom".
100236
+ const scrollingPixelThreshold = 10;
100237
+ // Slot name for messages that precede the current turn's anchor message.
100238
+ const historySlotName = 'history';
100239
+ /**
100240
+ * Manages auto-scroll behavior for the chat conversation:
100241
+ * - Splits messages into `default` and `history` slots so the top message of the
100242
+ * `default` slot becomes the anchor: it is moved to the top of the viewport and scrolled to on insert
100243
+ * - Implements auto scroll when at the bottom of the window as content is added until the user scrolls away
100244
+ * @internal
100245
+ */
100246
+ class AutoScrollManager {
100247
+ get isActive() {
100248
+ return this.resizeObserver !== undefined;
100249
+ }
100250
+ constructor(conversation) {
100251
+ this.conversation = conversation;
100252
+ /**
100253
+ * Whether auto-scroll is currently following new content. Set to false when
100254
+ * the user scrolls away from the bottom and back to true when they return.
100255
+ */
100256
+ this.autoScrollEngaged = true;
100257
+ /**
100258
+ * Whether the anchored region is reserving a viewport of space
100259
+ */
100260
+ this.anchorActive = false;
100261
+ this.scrollUpdatePending = false;
100262
+ this.pendingAnchorInsert = false;
100263
+ this.previousMessages = [];
100264
+ this.onScroll = () => {
100265
+ const container = this.conversation.messagesContainer;
100266
+ if (this.programmaticScrollTarget !== undefined) {
100267
+ // The programmatic scroll always targets the bottom, so treat it as
100268
+ // settled once we reach that target or the bottom itself.
100269
+ const reachedTarget = Math.abs(container.scrollTop - this.programmaticScrollTarget) <= 1;
100270
+ const reachedBottom = this.getDistanceFromBottom() <= scrollingPixelThreshold;
100271
+ if (reachedTarget || reachedBottom) {
100272
+ this.programmaticScrollTarget = undefined;
100273
+ }
100274
+ return;
100275
+ }
100276
+ this.autoScrollEngaged = this.getDistanceFromBottom() <= scrollingPixelThreshold;
100277
+ };
100278
+ this.conversationNotifier = Observable.getNotifier(this.conversation);
100279
+ }
100280
+ connect() {
100281
+ this.autoScrollEngaged = true;
100282
+ this.previousMessages = this.getOrderedMessages();
100283
+ this.conversationNotifier.subscribe(this, 'slottedMessages');
100284
+ this.conversationNotifier.subscribe(this, 'slottedHistoryMessages');
100285
+ this.conversation.messagesContainer.addEventListener('scroll', this.onScroll, { passive: true });
100286
+ this.resizeObserver = new ResizeObserver(() => {
100287
+ this.onContentSizeChanged();
100288
+ });
100289
+ // Observe the anchored region for streamed content growth and the scroll
100290
+ // viewport so the conversation stays pinned when its height changes.
100291
+ this.resizeObserver.observe(this.conversation.anchoredContainer);
100292
+ this.resizeObserver.observe(this.conversation.messagesContainer);
100293
+ this.repartition(this.previousMessages);
100294
+ }
100295
+ disconnect() {
100296
+ this.conversationNotifier.unsubscribe(this, 'slottedMessages');
100297
+ this.conversationNotifier.unsubscribe(this, 'slottedHistoryMessages');
100298
+ this.conversation.messagesContainer.removeEventListener('scroll', this.onScroll);
100299
+ this.resizeObserver?.disconnect();
100300
+ this.resizeObserver = undefined;
100301
+ this.setScrollAnchorMessage(undefined);
100302
+ this.clearSlotAssignments();
100303
+ this.anchorActive = false;
100304
+ this.previousMessages = [];
100305
+ }
100306
+ handleChange(source, args) {
100307
+ if (source === this.conversation
100308
+ && (args === 'slottedMessages' || args === 'slottedHistoryMessages')) {
100309
+ this.onMessagesChanged();
100310
+ }
100311
+ }
100312
+ onMessagesChanged() {
100313
+ const current = this.getOrderedMessages();
100314
+ const previousSet = new Set(this.previousMessages);
100315
+ const addedMessages = current.filter(message => !previousSet.has(message));
100316
+ this.previousMessages = current;
100317
+ this.repartition(current);
100318
+ if (addedMessages.length === 0) {
100319
+ return;
100320
+ }
100321
+ const hasAnchorMessage = addedMessages.some(message => message.messageInternals.anchorOnInsert);
100322
+ this.scheduleScrollUpdate(hasAnchorMessage);
100323
+ }
100324
+ repartition(messages) {
100325
+ const anchorIndex = this.findLatestAnchorIndex(messages);
100326
+ messages.forEach((message, index) => {
100327
+ message.messageInternals.slot = anchorIndex >= 0 && index < anchorIndex
100328
+ ? historySlotName
100329
+ : undefined;
100330
+ });
100331
+ this.anchorActive = anchorIndex >= 0;
100332
+ }
100333
+ clearSlotAssignments() {
100334
+ for (const message of this.getOrderedMessages()) {
100335
+ message.messageInternals.slot = undefined;
100336
+ }
100337
+ }
100338
+ scheduleScrollUpdate(hasAnchorMessage) {
100339
+ this.pendingAnchorInsert = this.pendingAnchorInsert || hasAnchorMessage;
100340
+ if (this.scrollUpdatePending) {
100341
+ return;
100342
+ }
100343
+ this.scrollUpdatePending = true;
100344
+ requestAnimationFrame(() => {
100345
+ this.scrollUpdatePending = false;
100346
+ const anchorInsert = this.pendingAnchorInsert;
100347
+ this.pendingAnchorInsert = false;
100348
+ if (anchorInsert) {
100349
+ this.anchorToLastInsertedMessage();
100350
+ }
100351
+ else if (this.autoScrollEngaged) {
100352
+ this.followContent();
100353
+ }
100354
+ });
100355
+ }
100356
+ /**
100357
+ * Pins the most recently inserted anchor message near the top of the
100358
+ * viewport.
100359
+ */
100360
+ anchorToLastInsertedMessage() {
100361
+ const message = this.getLastAnchorMessage();
100362
+ if (message === undefined) {
100363
+ return;
100364
+ }
100365
+ this.setScrollAnchorMessage(message);
100366
+ this.autoScrollEngaged = true;
100367
+ this.smoothScrollTo(this.getMaxScrollTop());
100368
+ }
100369
+ followContent() {
100370
+ this.instantScrollTo(this.getMaxScrollTop());
100371
+ }
100372
+ onContentSizeChanged() {
100373
+ // Reacts to streamed content growth and viewport height changes.
100374
+ // While a pending or in-progress anchor insert owns positioning, let its
100375
+ // smooth scroll settle instead of competing with an instant follow.
100376
+ if (this.pendingAnchorInsert
100377
+ || this.programmaticScrollTarget !== undefined) {
100378
+ return;
100379
+ }
100380
+ if (this.autoScrollEngaged) {
100381
+ this.followContent();
100382
+ }
100383
+ }
100384
+ getDistanceFromBottom() {
100385
+ const { scrollTop, scrollHeight, clientHeight } = this.conversation.messagesContainer;
100386
+ return scrollHeight - scrollTop - clientHeight;
100387
+ }
100388
+ getMaxScrollTop() {
100389
+ const { scrollHeight, clientHeight } = this.conversation.messagesContainer;
100390
+ return Math.max(0, scrollHeight - clientHeight);
100391
+ }
100392
+ smoothScrollTo(scrollTop) {
100393
+ const container = this.conversation.messagesContainer;
100394
+ if (Math.abs(container.scrollTop - scrollTop) <= 1) {
100395
+ // No movement is needed, so `scrollTo` would not emit a scroll event
100396
+ // to clear the programmatic guard. Snap to the exact target and
100397
+ // leave the guard clear so streamed content keeps being followed.
100398
+ this.programmaticScrollTarget = undefined;
100399
+ container.scrollTop = scrollTop;
100400
+ return;
100401
+ }
100402
+ this.programmaticScrollTarget = scrollTop;
100403
+ container.scrollTo({
100404
+ top: scrollTop,
100405
+ behavior: 'smooth'
100406
+ });
100407
+ }
100408
+ instantScrollTo(scrollTop) {
100409
+ this.conversation.messagesContainer.scrollTop = scrollTop;
100410
+ }
100411
+ setScrollAnchorMessage(message) {
100412
+ if (this.scrollAnchorMessage === message) {
100413
+ return;
100414
+ }
100415
+ if (this.scrollAnchorMessage !== undefined) {
100416
+ this.scrollAnchorMessage.messageInternals.isScrollAnchor = false;
100417
+ }
100418
+ this.scrollAnchorMessage = message;
100419
+ if (message !== undefined) {
100420
+ message.messageInternals.isScrollAnchor = true;
100421
+ }
100422
+ }
100423
+ getLastAnchorMessage() {
100424
+ const messages = this.getOrderedMessages();
100425
+ const index = this.findLatestAnchorIndex(messages);
100426
+ return index >= 0 ? messages[index] : undefined;
100427
+ }
100428
+ findLatestAnchorIndex(messages) {
100429
+ for (let i = messages.length - 1; i >= 0; i--) {
100430
+ const message = messages[i];
100431
+ if (message?.messageInternals.anchorOnInsert) {
100432
+ return i;
100433
+ }
100434
+ }
100435
+ return -1;
100436
+ }
100437
+ getOrderedMessages() {
100438
+ const messages = [];
100439
+ for (const child of Array.from(this.conversation.children)) {
100440
+ if (ChatMessageInternals.elementHasMessageInternals(child)) {
100441
+ messages.push(child);
100442
+ }
100443
+ }
100444
+ return messages;
100445
+ }
100446
+ }
100447
+ __decorate([
100448
+ observable
100449
+ ], AutoScrollManager.prototype, "autoScrollEngaged", void 0);
100450
+ __decorate([
100451
+ observable
100452
+ ], AutoScrollManager.prototype, "anchorActive", void 0);
100453
+
100174
100454
  /**
100175
100455
  * A Spright component for displaying a series of chat messages
100176
100456
  */
@@ -100178,6 +100458,15 @@ focus outline in that case.
100178
100458
  constructor() {
100179
100459
  super(...arguments);
100180
100460
  this.appearance = ChatConversationAppearance.default;
100461
+ this.autoScroll = false;
100462
+ /**
100463
+ * Manages auto-scroll behavior. Always present; its observers are registered
100464
+ * while the conversation is connected and `autoScroll` is enabled.
100465
+ * @internal
100466
+ */
100467
+ this.autoScrollManager = new AutoScrollManager(this);
100468
+ /** @internal */
100469
+ this.historyEmpty = true;
100181
100470
  /** @internal */
100182
100471
  this.inputEmpty = true;
100183
100472
  /** @internal */
@@ -100187,6 +100476,31 @@ focus outline in that case.
100187
100476
  /** @internal */
100188
100477
  this.endEmpty = true;
100189
100478
  }
100479
+ connectedCallback() {
100480
+ super.connectedCallback();
100481
+ if (this.autoScroll) {
100482
+ this.autoScrollManager.connect();
100483
+ }
100484
+ }
100485
+ disconnectedCallback() {
100486
+ super.disconnectedCallback();
100487
+ if (this.autoScroll) {
100488
+ this.autoScrollManager.disconnect();
100489
+ }
100490
+ }
100491
+ autoScrollChanged() {
100492
+ if (this.$fastController.isConnected) {
100493
+ if (this.autoScroll) {
100494
+ this.autoScrollManager.connect();
100495
+ }
100496
+ else {
100497
+ this.autoScrollManager.disconnect();
100498
+ }
100499
+ }
100500
+ }
100501
+ slottedHistoryMessagesChanged(_prev, next) {
100502
+ this.historyEmpty = next === undefined || next.length === 0;
100503
+ }
100190
100504
  slottedInputElementsChanged(_prev, next) {
100191
100505
  this.inputEmpty = next === undefined || next.length === 0;
100192
100506
  }
@@ -100203,6 +100517,18 @@ focus outline in that case.
100203
100517
  __decorate([
100204
100518
  attr
100205
100519
  ], ChatConversation.prototype, "appearance", void 0);
100520
+ __decorate([
100521
+ attr({ attribute: 'auto-scroll', mode: 'boolean' })
100522
+ ], ChatConversation.prototype, "autoScroll", void 0);
100523
+ __decorate([
100524
+ observable
100525
+ ], ChatConversation.prototype, "slottedMessages", void 0);
100526
+ __decorate([
100527
+ observable
100528
+ ], ChatConversation.prototype, "slottedHistoryMessages", void 0);
100529
+ __decorate([
100530
+ observable
100531
+ ], ChatConversation.prototype, "historyEmpty", void 0);
100206
100532
  __decorate([
100207
100533
  observable
100208
100534
  ], ChatConversation.prototype, "inputEmpty", void 0);
@@ -100862,6 +101188,8 @@ focus outline in that case.
100862
101188
  constructor() {
100863
101189
  super(...arguments);
100864
101190
  /** @internal */
101191
+ this.messageInternals = new ChatMessageInternals(this);
101192
+ /** @internal */
100865
101193
  this.footerActionsIsEmpty = true;
100866
101194
  }
100867
101195
  slottedFooterActionsElementsChanged(_prev, next) {
@@ -100928,6 +101256,13 @@ focus outline in that case.
100928
101256
  * A Spright component for displaying an outbound chat message
100929
101257
  */
100930
101258
  class ChatMessageOutbound extends FoundationElement {
101259
+ constructor() {
101260
+ super(...arguments);
101261
+ /** @internal */
101262
+ this.messageInternals = new ChatMessageInternals(this, {
101263
+ anchorOnInsert: true
101264
+ });
101265
+ }
100931
101266
  }
100932
101267
  const sprightChatMessageOutbound = ChatMessageOutbound.compose({
100933
101268
  baseName: 'chat-message-outbound',
@@ -100975,6 +101310,11 @@ focus outline in that case.
100975
101310
  * A Spright component for displaying an system chat message
100976
101311
  */
100977
101312
  class ChatMessageSystem extends FoundationElement {
101313
+ constructor() {
101314
+ super(...arguments);
101315
+ /** @internal */
101316
+ this.messageInternals = new ChatMessageInternals(this);
101317
+ }
100978
101318
  }
100979
101319
  const sprightChatMessageSystem = ChatMessageSystem.compose({
100980
101320
  baseName: 'chat-message-system',