@supersoniks/concorde 4.9.7 → 4.9.9

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 (31) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +180 -180
  3. package/concorde-core.es.js +3660 -3508
  4. package/dist/concorde-core.bundle.js +180 -180
  5. package/dist/concorde-core.es.js +3660 -3508
  6. package/dist/js/core/components/functional/queue/queue.js +25 -153
  7. package/dist/js/core/components/functional/queue/queue.js.map +1 -1
  8. package/dist/js/core/components/ui/_css/scroll.js +2 -0
  9. package/dist/js/core/components/ui/_css/scroll.js.map +1 -1
  10. package/dist/js/core/utils/SafariScrollGuard.js +384 -0
  11. package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
  12. package/dist/js/utils.js +3 -0
  13. package/dist/js/utils.js.map +1 -1
  14. package/dist/types/core/components/functional/queue/queue.d.ts +8 -24
  15. package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
  16. package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
  17. package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
  18. package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
  19. package/dist/types/utils.d.ts +1 -0
  20. package/dist/types/utils.d.ts.map +1 -1
  21. package/package.json +6 -2
  22. package/src/core/components/functional/queue/queue.ts +33 -164
  23. package/src/core/components/ui/_css/scroll.ts +3 -0
  24. package/src/core/utils/SafariScrollGuard.ts +429 -0
  25. package/src/docs/search/docs-search.json +28 -108
  26. package/src/docs/tailwind/css/tailwind.css +0 -0
  27. package/src/tsconfig.json +3 -0
  28. package/src/tsconfig.tsbuildinfo +1 -1
  29. package/src/utils.ts +13 -0
  30. package/vite.config.mts +4 -0
  31. package/vitest.setup.ts +22 -0
@@ -5,6 +5,13 @@ import { map } from "lit/directives/map.js";
5
5
  import DataProvider, { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
6
6
  import "@supersoniks/concorde/core/components/functional/list/list";
7
7
  import { HTML } from "@supersoniks/concorde/utils";
8
+ import {
9
+ isSafariScrollEngine,
10
+ resetScrollRoot,
11
+ restoreScrollRoot,
12
+ watchScrollRoot,
13
+ type ScrollRoot,
14
+ } from "@supersoniks/concorde/core/utils/SafariScrollGuard";
8
15
  import { ListItems } from "@supersoniks/concorde/core/components/functional/list/list";
9
16
 
10
17
  type QueueItem = {
@@ -74,7 +81,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
74
81
  this.requestId = 0;
75
82
  this.isFirstRequest = true;
76
83
  this.nextHadEvent = false;
77
- this.teardownScrollGuard();
84
+ this.teardownScrollSync();
78
85
  this.publisher.set({});
79
86
  super.disconnectedCallback();
80
87
  return;
@@ -106,8 +113,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
106
113
  HTML.getAncestorAttributeValue(this.parentElement, "dataProvider") ||
107
114
  "";
108
115
  }
109
- this.captureScrollPosition();
110
- this.ensureScrollGuard();
116
+ this.ensureScrollSync();
111
117
  super.connectedCallback();
112
118
  this.publisher.set({});
113
119
  this.key = this.getAttribute("key");
@@ -164,8 +170,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
164
170
  if (searchHash == this.searchHash && !this.isFirstRequest) return;
165
171
  this.searchHash = searchHash;
166
172
  // Nouvelle recherche : ne pas restaurer l'ancienne position de scroll
167
- this.storedScrollX = 0;
168
- this.storedScrollY = 0;
173
+ resetScrollRoot(this.resolveScrollRoot(), { x: 0, y: 0 });
169
174
 
170
175
  /**
171
176
  * on reset les données avant de lancer la requète
@@ -193,23 +198,15 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
193
198
  };
194
199
 
195
200
  /**
196
- * Conteneur scrollé (élément ou window) — Safari / iOS only.
197
- * Stock = dernière position user (touch/wheel/pointer + settle inertie).
198
- * Restore = updated (enfants Lit) + MutationObserver (subscribers / modal).
201
+ * Conteneur scrollé — Safari : stock/restore via SafariScrollGuard
202
+ * (capture intent user globale). Ici : sync root + restore après Lit.
199
203
  */
200
- scrollRoot: HTMLElement | Window | null = null;
201
- storedScrollY = 0;
202
- storedScrollX = 0;
203
- private static readonly SCROLL_SETTLE_MS = 150;
204
- private userScrollEngaged = false;
205
- private restoringScroll = false;
206
- private scrollGuardActive = false;
207
- private scrollSettleTimer?: ReturnType<typeof setTimeout>;
204
+ scrollRoot: ScrollRoot | null = null;
205
+ private scrollSyncActive = false;
208
206
  private mutationObserver: MutationObserver | null = null;
209
- private boundScrollTarget: EventTarget | null = null;
210
207
  private restoreGeneration = 0;
211
208
 
212
- resolveScrollRoot(): HTMLElement | Window {
209
+ resolveScrollRoot(): ScrollRoot {
213
210
  const ancestor = HTML.getScrollingAncestor(this);
214
211
  if (
215
212
  !ancestor ||
@@ -221,136 +218,21 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
221
218
  return ancestor as HTMLElement;
222
219
  }
223
220
 
224
- /** Lit la position courante — ne pas appeler sur un scroll “fantôme” Safari. */
225
- captureScrollPosition() {
226
- if (!this.isSafari()) {
227
- return;
228
- }
229
- const root = this.resolveScrollRoot();
230
- this.scrollRoot = root;
231
- if (root === window) {
232
- this.storedScrollX = window.scrollX;
233
- this.storedScrollY = window.scrollY;
234
- } else {
235
- const el = root as HTMLElement;
236
- this.storedScrollX = el.scrollLeft;
237
- this.storedScrollY = el.scrollTop;
238
- }
239
- }
240
-
241
- restoreScrollPosition() {
242
- if (!this.isSafari()) {
243
- return;
244
- }
245
- const root = this.scrollRoot ?? this.resolveScrollRoot();
246
- const x = this.storedScrollX;
247
- const y = this.storedScrollY;
248
- this.restoringScroll = true;
249
- try {
250
- if (root === window) {
251
- if (
252
- Math.abs(x - window.scrollX) > 10 ||
253
- Math.abs(y - window.scrollY) > 10
254
- ) {
255
- window.scrollTo(x, y);
256
- }
257
- } else {
258
- const el = root as HTMLElement;
259
- if (
260
- Math.abs(x - el.scrollLeft) > 10 ||
261
- Math.abs(y - el.scrollTop) > 10
262
- ) {
263
- el.scrollLeft = x;
264
- el.scrollTop = y;
265
- }
266
- }
267
- } finally {
268
- requestAnimationFrame(() => {
269
- this.restoringScroll = false;
270
- });
271
- }
272
- }
273
-
274
221
  isSafari() {
275
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
222
+ return isSafariScrollEngine();
276
223
  }
277
224
 
278
- private markUserScrollEngaged = () => {
279
- this.userScrollEngaged = true;
280
- };
281
-
282
- private armScrollSettle = () => {
283
- clearTimeout(this.scrollSettleTimer);
284
- this.scrollSettleTimer = setTimeout(() => {
285
- this.userScrollEngaged = false;
286
- this.scrollSettleTimer = undefined;
287
- }, Queue.SCROLL_SETTLE_MS);
288
- };
289
-
290
- private onScrollRootScroll = () => {
291
- if (!this.isSafari() || this.restoringScroll || !this.userScrollEngaged) {
292
- return;
293
- }
294
- this.captureScrollPosition();
295
- // iOS : l’inertie continue après touchend → prolonger l’engagement
296
- this.armScrollSettle();
297
- };
298
-
299
- private onUserScrollIntent = () => {
300
- if (!this.isSafari()) return;
301
- this.markUserScrollEngaged();
302
- };
303
-
304
- private onUserScrollIntentEnd = () => {
305
- if (!this.isSafari()) return;
306
- this.armScrollSettle();
307
- };
308
-
309
- private onWheelIntent = () => {
310
- if (!this.isSafari()) return;
311
- this.markUserScrollEngaged();
312
- this.armScrollSettle();
313
- };
314
-
315
- ensureScrollGuard() {
316
- if (!this.isSafari() || !this.isConnected) {
225
+ ensureScrollSync() {
226
+ if (!isSafariScrollEngine() || !this.isConnected) {
317
227
  return;
318
228
  }
319
229
  const root = this.resolveScrollRoot();
320
- const target: EventTarget = root === window ? window : root;
321
- if (this.scrollGuardActive && this.boundScrollTarget === target) {
322
- this.scrollRoot = root;
230
+ this.scrollRoot = root;
231
+ watchScrollRoot(root);
232
+ if (this.scrollSyncActive) {
323
233
  return;
324
234
  }
325
- this.teardownScrollGuard();
326
- this.scrollRoot = root;
327
- this.boundScrollTarget = target;
328
- this.scrollGuardActive = true;
329
-
330
- target.addEventListener("scroll", this.onScrollRootScroll, {
331
- passive: true,
332
- });
333
- // Intent user : touch (iOS), wheel, pointer (scrollbar desktop Safari)
334
- target.addEventListener("touchstart", this.onUserScrollIntent, {
335
- passive: true,
336
- });
337
- target.addEventListener("touchend", this.onUserScrollIntentEnd, {
338
- passive: true,
339
- });
340
- target.addEventListener("touchcancel", this.onUserScrollIntentEnd, {
341
- passive: true,
342
- });
343
- target.addEventListener("wheel", this.onWheelIntent, { passive: true });
344
- target.addEventListener("pointerdown", this.onUserScrollIntent, {
345
- passive: true,
346
- });
347
- target.addEventListener("pointerup", this.onUserScrollIntentEnd, {
348
- passive: true,
349
- });
350
- target.addEventListener("pointercancel", this.onUserScrollIntentEnd, {
351
- passive: true,
352
- });
353
-
235
+ this.scrollSyncActive = true;
354
236
  this.mutationObserver = new MutationObserver(() => {
355
237
  this.scheduleRestoreAfterChildren();
356
238
  });
@@ -361,27 +243,11 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
361
243
  });
362
244
  }
363
245
 
364
- teardownScrollGuard() {
246
+ teardownScrollSync() {
365
247
  this.restoreGeneration++;
366
- clearTimeout(this.scrollSettleTimer);
367
- this.scrollSettleTimer = undefined;
368
- this.userScrollEngaged = false;
369
- this.restoringScroll = false;
370
- if (this.boundScrollTarget) {
371
- const target = this.boundScrollTarget;
372
- target.removeEventListener("scroll", this.onScrollRootScroll);
373
- target.removeEventListener("touchstart", this.onUserScrollIntent);
374
- target.removeEventListener("touchend", this.onUserScrollIntentEnd);
375
- target.removeEventListener("touchcancel", this.onUserScrollIntentEnd);
376
- target.removeEventListener("wheel", this.onWheelIntent);
377
- target.removeEventListener("pointerdown", this.onUserScrollIntent);
378
- target.removeEventListener("pointerup", this.onUserScrollIntentEnd);
379
- target.removeEventListener("pointercancel", this.onUserScrollIntentEnd);
380
- this.boundScrollTarget = null;
381
- }
382
248
  this.mutationObserver?.disconnect();
383
249
  this.mutationObserver = null;
384
- this.scrollGuardActive = false;
250
+ this.scrollSyncActive = false;
385
251
  this.scrollRoot = null;
386
252
  }
387
253
 
@@ -390,7 +256,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
390
256
  * Ne pas await updateComplete *dans* updated (deadlock) — on sort du cycle via void async.
391
257
  */
392
258
  scheduleRestoreAfterChildren() {
393
- if (!this.isSafari() || !this.isConnected) {
259
+ if (!isSafariScrollEngine() || !this.isConnected) {
394
260
  return;
395
261
  }
396
262
  const generation = ++this.restoreGeneration;
@@ -410,14 +276,17 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
410
276
  await new Promise<void>((r) => requestAnimationFrame(() => r()));
411
277
  if (generation !== this.restoreGeneration || !this.isConnected) return;
412
278
 
413
- this.restoreScrollPosition();
279
+ const root = this.scrollRoot ?? this.resolveScrollRoot();
280
+ watchScrollRoot(root);
281
+ restoreScrollRoot(root);
414
282
  })();
415
283
  }
416
284
 
417
285
  protected updated(_changedProperties: PropertyValues): void {
418
- if (this.isSafari()) {
419
- this.ensureScrollGuard();
420
- this.restoreScrollPosition();
286
+ if (isSafariScrollEngine()) {
287
+ this.ensureScrollSync();
288
+ const root = this.scrollRoot ?? this.resolveScrollRoot();
289
+ restoreScrollRoot(root);
421
290
  this.scheduleRestoreAfterChildren();
422
291
  }
423
292
 
@@ -533,7 +402,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
533
402
  // Ne pas écraser le stock avec un jump Safari : uniquement sync du root / guard.
534
403
  if (this.isSafari()) {
535
404
  this.scrollRoot = this.resolveScrollRoot();
536
- this.ensureScrollGuard();
405
+ this.ensureScrollSync();
537
406
  }
538
407
  if (!Array.isArray(this.props)) {
539
408
  return nothing;
@@ -1,4 +1,7 @@
1
1
  import { css } from "lit";
2
+ import { installSafariScrollGuard } from "@supersoniks/concorde/core/utils/SafariScrollGuard";
3
+
4
+ installSafariScrollGuard();
2
5
 
3
6
  export const customScroll = css`
4
7
  .custom-scroll {