@bbki.ng/bb-msg-history 0.11.1 → 0.12.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.
@@ -5,6 +5,9 @@ export declare class BBMsgHistory extends HTMLElement {
5
5
  private _lastAuthor;
6
6
  private _lastGroupTimestamp;
7
7
  private _scrollButtonVisible;
8
+ private _userHasScrolledManually;
9
+ private _isProgrammaticScroll;
10
+ private _lastScrollTop;
8
11
  static get observedAttributes(): string[];
9
12
  constructor();
10
13
  attributeChangedCallback(name: string): void;
package/dist/component.js CHANGED
@@ -6,17 +6,20 @@ import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-bui
6
6
  import { buildScrollButtonHtml } from './utils/scroll-button.js';
7
7
  export class BBMsgHistory extends HTMLElement {
8
8
  static get observedAttributes() {
9
- return ['theme', 'loading', 'hide-scroll-bar', 'infinite'];
9
+ return ['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'];
10
10
  }
11
11
  constructor() {
12
12
  super();
13
13
  this._userAuthors = new Map();
14
14
  this._lastAuthor = '';
15
15
  this._scrollButtonVisible = false;
16
+ this._userHasScrolledManually = false;
17
+ this._isProgrammaticScroll = false;
18
+ this._lastScrollTop = 0;
16
19
  this.attachShadow({ mode: 'open' });
17
20
  }
18
21
  attributeChangedCallback(name) {
19
- if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite') {
22
+ if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite' || name === 'hide-scroll-button') {
20
23
  this.render();
21
24
  }
22
25
  }
@@ -115,15 +118,27 @@ export class BBMsgHistory extends HTMLElement {
115
118
  }
116
119
  // Smooth scroll to bottom (skip in infinite mode)
117
120
  if (!this.hasAttribute('infinite')) {
121
+ // Mark this as a programmatic scroll so the scroll handler ignores it
122
+ this._isProgrammaticScroll = true;
118
123
  container.scrollTo({
119
124
  top: container.scrollHeight,
120
125
  behavior: 'smooth',
121
126
  });
127
+ // Reset the flag after smooth scroll animation completes (~300ms)
128
+ setTimeout(() => {
129
+ this._isProgrammaticScroll = false;
130
+ }, 300);
122
131
  // Hide scroll button since we're scrolling to bottom
123
132
  const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
124
133
  if (scrollButton && this._scrollButtonVisible) {
125
134
  this._scrollButtonVisible = false;
126
135
  scrollButton.classList.remove('visible');
136
+ // Dispatch hide event
137
+ this.dispatchEvent(new CustomEvent('bb-scrollbuttonhide', {
138
+ bubbles: true,
139
+ composed: true,
140
+ detail: { visible: false }
141
+ }));
127
142
  }
128
143
  }
129
144
  }
@@ -224,12 +239,13 @@ export class BBMsgHistory extends HTMLElement {
224
239
  <div class="loading-spinner"></div>
225
240
  </div>`
226
241
  : '';
242
+ const hideScrollButton = this.hasAttribute('hide-scroll-button');
227
243
  this.shadowRoot.innerHTML = `
228
244
  <style>${MAIN_STYLES}${LOADING_STYLES}</style>
229
245
  <div class="history" role="log" aria-live="polite" aria-label="Message history">
230
246
  ${messagesHtml}
231
247
  </div>
232
- ${buildScrollButtonHtml()}
248
+ ${hideScrollButton ? '' : buildScrollButtonHtml()}
233
249
  ${loadingOverlay}
234
250
  `;
235
251
  this._setupAfterRender();
@@ -267,10 +283,18 @@ export class BBMsgHistory extends HTMLElement {
267
283
  _setupAfterRender() {
268
284
  requestAnimationFrame(() => {
269
285
  const container = this.shadowRoot.querySelector('.history');
270
- const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
286
+ const hideScrollButton = this.hasAttribute('hide-scroll-button');
287
+ const scrollButton = hideScrollButton
288
+ ? null
289
+ : this.shadowRoot.querySelector('.scroll-to-bottom');
271
290
  const isInfinite = this.hasAttribute('infinite');
272
- if (container && !isInfinite) {
291
+ if (container && !isInfinite && !hideScrollButton) {
292
+ // Mark as programmatic scroll to prevent triggering user scroll detection
293
+ this._isProgrammaticScroll = true;
273
294
  container.scrollTop = container.scrollHeight;
295
+ requestAnimationFrame(() => {
296
+ this._isProgrammaticScroll = false;
297
+ });
274
298
  this._setupScrollTracking(container, scrollButton, { skipInitialCheck: true });
275
299
  }
276
300
  if (scrollButton && !isInfinite) {
@@ -306,15 +330,34 @@ export class BBMsgHistory extends HTMLElement {
306
330
  }
307
331
  _setupScrollTracking(container, button, options) {
308
332
  const checkScrollPosition = () => {
333
+ // Ignore programmatic scrolls - they don't indicate user intent
334
+ if (this._isProgrammaticScroll)
335
+ return;
336
+ // Mark that user has manually scrolled
337
+ if (!this._userHasScrolledManually) {
338
+ this._userHasScrolledManually = true;
339
+ }
340
+ const currentScrollTop = container.scrollTop;
341
+ const isScrollingUp = currentScrollTop < this._lastScrollTop;
342
+ this._lastScrollTop = currentScrollTop;
309
343
  const threshold = 50; // pixels from bottom
310
344
  const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
311
345
  const hasOverflow = container.scrollHeight > container.clientHeight;
312
- const shouldShow = !isAtBottom && hasOverflow;
346
+ // Only show button when: user has scrolled, is not at bottom, is scrolling up
347
+ const shouldShow = !isAtBottom && hasOverflow && this._userHasScrolledManually && isScrollingUp;
313
348
  if (shouldShow !== this._scrollButtonVisible) {
314
349
  this._scrollButtonVisible = shouldShow;
315
350
  button.classList.toggle('visible', shouldShow);
351
+ // Dispatch custom event
352
+ this.dispatchEvent(new CustomEvent(shouldShow ? 'bb-scrollbuttonshow' : 'bb-scrollbuttonhide', {
353
+ bubbles: true,
354
+ composed: true,
355
+ detail: { visible: shouldShow }
356
+ }));
316
357
  }
317
358
  };
359
+ // Initialize last scroll position
360
+ this._lastScrollTop = container.scrollTop;
318
361
  // Check initial state unless skipped
319
362
  if (!options?.skipInitialCheck) {
320
363
  checkScrollPosition();
@@ -71,6 +71,11 @@ export const MAIN_STYLES = `
71
71
  display: none;
72
72
  }
73
73
 
74
+ /* Hide scroll button when explicitly disabled */
75
+ :host([hide-scroll-button]) .scroll-to-bottom {
76
+ display: none;
77
+ }
78
+
74
79
  /* Scroll to bottom button */
75
80
  .scroll-to-bottom {
76
81
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/bb-msg-history",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "A chat-style message history web component",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/component.ts CHANGED
@@ -12,9 +12,12 @@ export class BBMsgHistory extends HTMLElement {
12
12
  private _lastAuthor = '';
13
13
  private _lastGroupTimestamp: string | undefined;
14
14
  private _scrollButtonVisible = false;
15
+ private _userHasScrolledManually = false;
16
+ private _isProgrammaticScroll = false;
17
+ private _lastScrollTop = 0;
15
18
 
16
19
  static get observedAttributes() {
17
- return ['theme', 'loading', 'hide-scroll-bar', 'infinite'];
20
+ return ['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'];
18
21
  }
19
22
 
20
23
  constructor() {
@@ -23,7 +26,7 @@ export class BBMsgHistory extends HTMLElement {
23
26
  }
24
27
 
25
28
  attributeChangedCallback(name: string) {
26
- if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite') {
29
+ if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite' || name === 'hide-scroll-button') {
27
30
  this.render();
28
31
  }
29
32
  }
@@ -149,16 +152,33 @@ export class BBMsgHistory extends HTMLElement {
149
152
 
150
153
  // Smooth scroll to bottom (skip in infinite mode)
151
154
  if (!this.hasAttribute('infinite')) {
155
+ // Mark this as a programmatic scroll so the scroll handler ignores it
156
+ this._isProgrammaticScroll = true;
157
+
152
158
  container.scrollTo({
153
159
  top: container.scrollHeight,
154
160
  behavior: 'smooth',
155
161
  });
156
162
 
163
+ // Reset the flag after smooth scroll animation completes (~300ms)
164
+ setTimeout(() => {
165
+ this._isProgrammaticScroll = false;
166
+ }, 300);
167
+
157
168
  // Hide scroll button since we're scrolling to bottom
158
169
  const scrollButton = this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement;
159
170
  if (scrollButton && this._scrollButtonVisible) {
160
171
  this._scrollButtonVisible = false;
161
172
  scrollButton.classList.remove('visible');
173
+
174
+ // Dispatch hide event
175
+ this.dispatchEvent(
176
+ new CustomEvent('bb-scrollbuttonhide', {
177
+ bubbles: true,
178
+ composed: true,
179
+ detail: { visible: false }
180
+ })
181
+ );
162
182
  }
163
183
  }
164
184
  }
@@ -282,12 +302,14 @@ export class BBMsgHistory extends HTMLElement {
282
302
  </div>`
283
303
  : '';
284
304
 
305
+ const hideScrollButton = this.hasAttribute('hide-scroll-button');
306
+
285
307
  this.shadowRoot!.innerHTML = `
286
308
  <style>${MAIN_STYLES}${LOADING_STYLES}</style>
287
309
  <div class="history" role="log" aria-live="polite" aria-label="Message history">
288
310
  ${messagesHtml}
289
311
  </div>
290
- ${buildScrollButtonHtml()}
312
+ ${hideScrollButton ? '' : buildScrollButtonHtml()}
291
313
  ${loadingOverlay}
292
314
  `;
293
315
 
@@ -334,12 +356,20 @@ export class BBMsgHistory extends HTMLElement {
334
356
  private _setupAfterRender(): void {
335
357
  requestAnimationFrame(() => {
336
358
  const container = this.shadowRoot!.querySelector('.history') as HTMLElement;
337
- const scrollButton = this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement;
359
+ const hideScrollButton = this.hasAttribute('hide-scroll-button');
360
+ const scrollButton = hideScrollButton
361
+ ? null
362
+ : (this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement);
338
363
  const isInfinite = this.hasAttribute('infinite');
339
364
 
340
- if (container && !isInfinite) {
365
+ if (container && !isInfinite && !hideScrollButton) {
366
+ // Mark as programmatic scroll to prevent triggering user scroll detection
367
+ this._isProgrammaticScroll = true;
341
368
  container.scrollTop = container.scrollHeight;
342
- this._setupScrollTracking(container, scrollButton, { skipInitialCheck: true });
369
+ requestAnimationFrame(() => {
370
+ this._isProgrammaticScroll = false;
371
+ });
372
+ this._setupScrollTracking(container, scrollButton!, { skipInitialCheck: true });
343
373
  }
344
374
 
345
375
  if (scrollButton && !isInfinite) {
@@ -382,18 +412,43 @@ export class BBMsgHistory extends HTMLElement {
382
412
  options?: { skipInitialCheck?: boolean }
383
413
  ): void {
384
414
  const checkScrollPosition = () => {
415
+ // Ignore programmatic scrolls - they don't indicate user intent
416
+ if (this._isProgrammaticScroll) return;
417
+
418
+ // Mark that user has manually scrolled
419
+ if (!this._userHasScrolledManually) {
420
+ this._userHasScrolledManually = true;
421
+ }
422
+
423
+ const currentScrollTop = container.scrollTop;
424
+ const isScrollingUp = currentScrollTop < this._lastScrollTop;
425
+ this._lastScrollTop = currentScrollTop;
426
+
385
427
  const threshold = 50; // pixels from bottom
386
428
  const isAtBottom =
387
429
  container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
388
430
  const hasOverflow = container.scrollHeight > container.clientHeight;
389
- const shouldShow = !isAtBottom && hasOverflow;
431
+ // Only show button when: user has scrolled, is not at bottom, is scrolling up
432
+ const shouldShow = !isAtBottom && hasOverflow && this._userHasScrolledManually && isScrollingUp;
390
433
 
391
434
  if (shouldShow !== this._scrollButtonVisible) {
392
435
  this._scrollButtonVisible = shouldShow;
393
436
  button.classList.toggle('visible', shouldShow);
437
+
438
+ // Dispatch custom event
439
+ this.dispatchEvent(
440
+ new CustomEvent(shouldShow ? 'bb-scrollbuttonshow' : 'bb-scrollbuttonhide', {
441
+ bubbles: true,
442
+ composed: true,
443
+ detail: { visible: shouldShow }
444
+ })
445
+ );
394
446
  }
395
447
  };
396
448
 
449
+ // Initialize last scroll position
450
+ this._lastScrollTop = container.scrollTop;
451
+
397
452
  // Check initial state unless skipped
398
453
  if (!options?.skipInitialCheck) {
399
454
  checkScrollPosition();
@@ -72,6 +72,11 @@ export const MAIN_STYLES = `
72
72
  display: none;
73
73
  }
74
74
 
75
+ /* Hide scroll button when explicitly disabled */
76
+ :host([hide-scroll-button]) .scroll-to-bottom {
77
+ display: none;
78
+ }
79
+
75
80
  /* Scroll to bottom button */
76
81
  .scroll-to-bottom {
77
82
  position: absolute;