@supersoniks/concorde 4.5.0 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -220,6 +220,9 @@
220
220
  "./modal-title": "./src/core/components/ui/modal/modal-title.ts",
221
221
  "./ui/modal-title": "./src/core/components/ui/modal/modal-title.ts",
222
222
  "./ui/modal/modal-title": "./src/core/components/ui/modal/modal-title.ts",
223
+ "./modal-utils": "./src/core/components/ui/modal/modal-utils.ts",
224
+ "./ui/modal-utils": "./src/core/components/ui/modal/modal-utils.ts",
225
+ "./ui/modal/modal-utils": "./src/core/components/ui/modal/modal-utils.ts",
223
226
  "./modal": "./src/core/components/ui/modal/modal.ts",
224
227
  "./ui/modal": "./src/core/components/ui/modal/modal.ts",
225
228
  "./pop": "./src/core/components/ui/pop/pop.ts",
@@ -283,6 +286,7 @@
283
286
  "./decorators/subscriber/bind": "./src/core/decorators/subscriber/bind.ts",
284
287
  "./decorators/subscriber/common": "./src/core/decorators/subscriber/common.ts",
285
288
  "./decorators/subscriber/dynamicPath": "./src/core/decorators/subscriber/dynamicPath.ts",
289
+ "./decorators/subscriber/dynamicPropertyWatch.spec": "./src/core/decorators/subscriber/dynamicPropertyWatch.spec.ts",
286
290
  "./decorators/subscriber/dynamicPropertyWatch": "./src/core/decorators/subscriber/dynamicPropertyWatch.ts",
287
291
  "./decorators/subscriber/onAssign": "./src/core/decorators/subscriber/onAssign.ts",
288
292
  "./decorators/subscriber/publish.spec": "./src/core/decorators/subscriber/publish.spec.ts",
@@ -0,0 +1,46 @@
1
+ import { CSSResultGroup, CSSResult } from "lit";
2
+
3
+ export type ModalStyleSheetInput =
4
+ | CSSResultGroup
5
+ | CSSStyleSheet
6
+ | Array<CSSResultGroup | CSSStyleSheet>;
7
+
8
+ const styleSheetCache = new WeakMap<CSSResult, CSSStyleSheet>();
9
+
10
+ function cssResultToStyleSheet(styleResult: CSSResult): CSSStyleSheet {
11
+ const cachedStyleSheet = styleSheetCache.get(styleResult);
12
+ if (cachedStyleSheet) return cachedStyleSheet;
13
+
14
+ const styleSheet = new CSSStyleSheet();
15
+ styleSheet.replaceSync(styleResult.cssText);
16
+ styleSheetCache.set(styleResult, styleSheet);
17
+ return styleSheet;
18
+ }
19
+
20
+ function isCSSResult(value: unknown): value is CSSResult {
21
+ return value instanceof CSSResult;
22
+ }
23
+
24
+ export function normalizeStyleSheets(
25
+ styleSheet?: ModalStyleSheetInput
26
+ ): Array<CSSStyleSheet> {
27
+ if (!styleSheet) return [];
28
+
29
+ const styleSheets = Array.isArray(styleSheet) ? styleSheet : [styleSheet];
30
+
31
+ return styleSheets.flatMap((sheet) => {
32
+ if (Array.isArray(sheet)) {
33
+ return normalizeStyleSheets(sheet);
34
+ }
35
+
36
+ if (sheet instanceof CSSStyleSheet) {
37
+ return [sheet];
38
+ }
39
+
40
+ if (isCSSResult(sheet)) {
41
+ return [cssResultToStyleSheet(sheet)];
42
+ }
43
+
44
+ return [];
45
+ });
46
+ }
@@ -19,6 +19,7 @@ import LocationHandler from "@supersoniks/concorde/core/utils/LocationHandler";
19
19
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
20
20
  import { ifDefined } from "lit/directives/if-defined.js";
21
21
  import { ButtonType } from "../button/button";
22
+ import { ModalStyleSheetInput, normalizeStyleSheets } from "./modal-utils";
22
23
 
23
24
  declare const window: ConcordeWindow;
24
25
  declare type ModalCreateOptions = {
@@ -26,6 +27,7 @@ declare type ModalCreateOptions = {
26
27
  subtitle?: string | DirectiveResult;
27
28
  content?: string | DirectiveResult;
28
29
  actions?: string | DirectiveResult;
30
+ styleSheet?: ModalStyleSheetInput;
29
31
  zIndex?: string;
30
32
  paddingX?: string;
31
33
  paddingY?: string;
@@ -171,7 +173,8 @@ export class Modal extends Subscriber(LitElement) {
171
173
  z-index: 20;
172
174
  opacity: 0;
173
175
  transform: scale(0);
174
- transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
176
+ transition:
177
+ transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
175
178
  opacity 0.1s linear;
176
179
  transform-origin: center center;
177
180
  display: flex;
@@ -223,6 +226,7 @@ export class Modal extends Subscriber(LitElement) {
223
226
  @property({ type: String }) width = "100%";
224
227
  @property({ type: String }) height = "fit-content";
225
228
  @property({ type: String }) effect: effectType = "slide";
229
+ @property({ attribute: false }) styleSheet?: ModalStyleSheetInput;
226
230
 
227
231
  @property({ type: Object }) options?: ModalCreateOptions;
228
232
 
@@ -237,6 +241,7 @@ export class Modal extends Subscriber(LitElement) {
237
241
 
238
242
  @state() private _animationState: "visible" | "in" | "out" | "hidden" =
239
243
  "hidden";
244
+ private _adoptedStyleSheets: Array<CSSStyleSheet> = [];
240
245
 
241
246
  static create(options: ModalCreateOptions): Modal {
242
247
  const modal = document.createElement(tagName) as Modal;
@@ -260,6 +265,7 @@ export class Modal extends Subscriber(LitElement) {
260
265
  if (options.noCloseButton) modal.noCloseButton = true;
261
266
  if (options.closeButtonType)
262
267
  modal.closeButtonType = options?.closeButtonType;
268
+ if (options.styleSheet) modal.styleSheet = options?.styleSheet;
263
269
  if (options.paddingX) modal.paddingX = options?.paddingX;
264
270
  if (options.paddingY) modal.paddingY = options?.paddingY;
265
271
  if (options.zIndex) modal.zIndex = options?.zIndex;
@@ -327,6 +333,10 @@ export class Modal extends Subscriber(LitElement) {
327
333
  } else if (changedToHidden && this._animationState === "visible") {
328
334
  this.hide();
329
335
  }
336
+
337
+ if (_changedProperties.has("styleSheet")) {
338
+ this.syncAdoptedStyleSheets();
339
+ }
330
340
  }
331
341
 
332
342
  // SI c'est en modal
@@ -393,6 +403,21 @@ export class Modal extends Subscriber(LitElement) {
393
403
  `;
394
404
  }
395
405
 
406
+ syncAdoptedStyleSheets() {
407
+ if (!(this.renderRoot instanceof ShadowRoot)) return;
408
+
409
+ const nextStyleSheets = normalizeStyleSheets(this.styleSheet);
410
+ const currentStyleSheets = this.renderRoot.adoptedStyleSheets.filter(
411
+ (sheet) => !this._adoptedStyleSheets.includes(sheet),
412
+ );
413
+
414
+ this.renderRoot.adoptedStyleSheets = [
415
+ ...currentStyleSheets,
416
+ ...nextStyleSheets,
417
+ ];
418
+ this._adoptedStyleSheets = nextStyleSheets;
419
+ }
420
+
396
421
  modalFragment(optionKey: keyof ModalCreateOptions) {
397
422
  const optionValue: Object | PrimitiveType = this.options?.[optionKey];
398
423
  if (!optionValue) return nothing;
@@ -440,7 +465,7 @@ export class Modal extends Subscriber(LitElement) {
440
465
  this.visible = false;
441
466
  if (this.hasAttribute("resetDataProviderOnHide")) {
442
467
  PublisherManager.get(this.getAttribute("resetDataProviderOnHide")).set(
443
- {}
468
+ {},
444
469
  );
445
470
  }
446
471
  if (this.removeHashOnHide) {
@@ -470,7 +495,7 @@ export class Modal extends Subscriber(LitElement) {
470
495
  if (e.key === "Escape") {
471
496
  e.preventDefault();
472
497
  const visibleModals = Modal.modals.filter(
473
- (modal) => modal._animationState !== "hidden" && !modal.forceAction
498
+ (modal) => modal._animationState !== "hidden" && !modal.forceAction,
474
499
  );
475
500
  if (visibleModals.length > 0) {
476
501
  const lastVisibleModal = visibleModals[visibleModals.length - 1];
@@ -529,8 +554,8 @@ export class Modal extends Subscriber(LitElement) {
529
554
  easing: isEntering ? quartOut : quadOut,
530
555
  fill: "both",
531
556
  delay: delay,
532
- }
533
- )
557
+ },
558
+ ),
534
559
  );
535
560
  animations.push(
536
561
  element.animate(
@@ -540,13 +565,13 @@ export class Modal extends Subscriber(LitElement) {
540
565
  easing: linear,
541
566
  fill: "both",
542
567
  delay: delay,
543
- }
544
- )
568
+ },
569
+ ),
545
570
  );
546
571
  }
547
572
 
548
573
  Promise.all(animations.map((anim) => anim.finished)).then(() =>
549
- resolve()
574
+ resolve(),
550
575
  );
551
576
  });
552
577
  }
@@ -18,7 +18,7 @@ import {
18
18
  } from "./subscriber/dynamicPath";
19
19
  import {
20
20
  getDynamicWatchKeys,
21
- registerDynamicPropertyWatcher,
21
+ observeDynamicProperty,
22
22
  } from "./subscriber/dynamicPropertyWatch";
23
23
  import { getPublisherFromPath } from "./subscriber/publisherPath";
24
24
 
@@ -203,7 +203,7 @@ export function get<T, Ue = any, Uk = any>(
203
203
 
204
204
  if (usesPublisherConfig) {
205
205
  for (const dependency of mergedDynamicDependencies) {
206
- const unsubscribe = registerDynamicPropertyWatcher(
206
+ const unsubscribe = observeDynamicProperty(
207
207
  getDynamicWatchKeys.watcherStore,
208
208
  getDynamicWatchKeys.hooked,
209
209
  component,
@@ -216,7 +216,7 @@ export function get<T, Ue = any, Uk = any>(
216
216
  } else {
217
217
  if (isDynamicPath) {
218
218
  for (const dependency of endpointDynamicDependencies) {
219
- const unsubscribe = registerDynamicPropertyWatcher(
219
+ const unsubscribe = observeDynamicProperty(
220
220
  getDynamicWatchKeys.watcherStore,
221
221
  getDynamicWatchKeys.hooked,
222
222
  component,
@@ -5,13 +5,14 @@ export function ancestorAttribute(attributeName: string) {
5
5
  return function (target: unknown, propertyKey: string) {
6
6
  if (!target) return;
7
7
  setSubscribable(target);
8
-
9
- (target as ConnectedComponent).__onConnected__((component) => {
8
+ (target as ConnectedComponent).__onBeforeConnected__((component) => {
10
9
  const value = HTML.getAncestorAttributeValue(
11
10
  component as any,
12
- attributeName
11
+ attributeName,
13
12
  );
14
- component[propertyKey] = value;
13
+ if (value !== null) {
14
+ component[propertyKey] = value;
15
+ }
15
16
  });
16
17
  };
17
18
  }
@@ -10,13 +10,13 @@ import {
10
10
  } from "./dynamicPath";
11
11
  import {
12
12
  bindDynamicWatchKeys,
13
- registerDynamicPropertyWatcher,
13
+ observeDynamicProperty,
14
14
  } from "./dynamicPropertyWatch";
15
15
  import { getPublisherFromPath } from "./publisherPath";
16
16
 
17
17
  function bindImpl(
18
18
  path: string,
19
- options?: { reflect?: boolean }
19
+ options?: { reflect?: boolean },
20
20
  ): (target: unknown, propertyKey: string) => void {
21
21
  const reflect = options?.reflect ?? false;
22
22
  const dynamicDependencies = extractDynamicDependencies(path);
@@ -34,7 +34,7 @@ function bindImpl(
34
34
  if (reflect) {
35
35
  const existingDescriptor = Object.getOwnPropertyDescriptor(
36
36
  target as any,
37
- propertyKey
37
+ propertyKey,
38
38
  );
39
39
  const internalValueKey = `__bind_${propertyKey}_value__`;
40
40
  const reflectUpdateFlagKey = `__bind_${propertyKey}_updating_from_publisher__`;
@@ -152,12 +152,12 @@ function bindImpl(
152
152
 
153
153
  if (isDynamicPath) {
154
154
  for (const dependency of dynamicDependencies) {
155
- const unsubscribe = registerDynamicPropertyWatcher(
155
+ const unsubscribe = observeDynamicProperty(
156
156
  bindDynamicWatchKeys.watcherStore,
157
157
  bindDynamicWatchKeys.hooked,
158
158
  component,
159
159
  dependency,
160
- () => refreshSubscription()
160
+ () => refreshSubscription(),
161
161
  );
162
162
  state.cleanupWatchers.push(unsubscribe);
163
163
  }
@@ -199,7 +199,10 @@ function bindImpl(
199
199
  * @state()
200
200
  * count: number = 0;
201
201
  */
202
- export function bind(path: string, options?: { reflect?: boolean }): (target: unknown, propertyKey: string) => void;
202
+ export function bind(
203
+ path: string,
204
+ options?: { reflect?: boolean },
205
+ ): (target: unknown, propertyKey: string) => void;
203
206
  export function bind<T, U = any>(
204
207
  key: DataProviderKey<T, U>,
205
208
  options?: { reflect?: boolean },
@@ -214,4 +217,3 @@ export function bind(
214
217
  const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
215
218
  return bindImpl(path, options);
216
219
  }
217
-
@@ -1,12 +1,26 @@
1
1
  type ConnectedCallback = (component: ConnectedComponent) => void;
2
2
 
3
3
  export type ConnectedComponent = Record<string, unknown> & {
4
+ /** Hooks exécutés avant le connectedCallback d'origine (ex. lecture DOM ancêtre). */
5
+ __onBeforeConnected__: (callback: ConnectedCallback) => void;
6
+ /** Hooks exécutés après le connectedCallback d'origine (ex. abonnements). */
4
7
  __onConnected__: (callback: ConnectedCallback) => void;
5
8
  __onDisconnected__: (callback: ConnectedCallback) => void;
9
+ __beforeConnectedCallbackCalls__?: Set<ConnectedCallback>;
6
10
  __connectedCallbackCalls__?: Set<ConnectedCallback>;
7
11
  __disconnectedCallbackCalls__?: Set<ConnectedCallback>;
8
12
  };
9
13
 
14
+ function onBeforeConnected(
15
+ this: ConnectedComponent,
16
+ callback: ConnectedCallback,
17
+ ) {
18
+ if (!this.__beforeConnectedCallbackCalls__) {
19
+ this.__beforeConnectedCallbackCalls__ = new Set();
20
+ }
21
+ this.__beforeConnectedCallbackCalls__.add(callback);
22
+ }
23
+
10
24
  function onConnected(this: ConnectedComponent, callback: ConnectedCallback) {
11
25
  if (!this.__connectedCallbackCalls__) {
12
26
  this.__connectedCallbackCalls__ = new Set();
@@ -25,15 +39,21 @@ export function setSubscribable(target: any) {
25
39
  if (target.__is__setSubscribable__) return;
26
40
  target.__is__setSubscribable__ = true;
27
41
 
42
+ target.__onBeforeConnected__ = onBeforeConnected;
28
43
  target.__onConnected__ = onConnected;
29
44
  target.__onDisconnected__ = onDisconnected;
30
45
 
31
46
  const originalConnectedCallback = target.connectedCallback;
32
47
  target.connectedCallback = function (this: any) {
48
+ if (this.__beforeConnectedCallbackCalls__) {
49
+ this.__beforeConnectedCallbackCalls__.forEach(
50
+ (callback: ConnectedCallback) => callback(this),
51
+ );
52
+ }
33
53
  originalConnectedCallback?.call(this);
34
54
  if (this.__connectedCallbackCalls__) {
35
55
  this.__connectedCallbackCalls__.forEach((callback: ConnectedCallback) =>
36
- callback(this)
56
+ callback(this),
37
57
  );
38
58
  }
39
59
  };
@@ -43,7 +63,7 @@ export function setSubscribable(target: any) {
43
63
  originalDisconnectedCallback?.call(this);
44
64
  if (this.__disconnectedCallbackCalls__) {
45
65
  this.__disconnectedCallbackCalls__.forEach(
46
- (callback: ConnectedCallback) => callback(this)
66
+ (callback: ConnectedCallback) => callback(this),
47
67
  );
48
68
  }
49
69
  };
@@ -0,0 +1,125 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { observeDynamicProperty } from "./dynamicPropertyWatch";
3
+
4
+ const legacyStoreKey = Symbol("legacyStore");
5
+ const legacyHookedKey = Symbol("legacyHooked");
6
+
7
+ async function flushFrames(count = 2): Promise<void> {
8
+ for (let i = 0; i < count; i += 1) {
9
+ await new Promise<void>((resolve) => {
10
+ requestAnimationFrame(() => resolve());
11
+ });
12
+ }
13
+ }
14
+
15
+ describe("observeDynamicProperty (poll rAF)", () => {
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it("déclenche le handler quand une prop observée change", async () => {
21
+ const component = { userIndex: 0 };
22
+ const onDependencyChange = vi.fn();
23
+
24
+ observeDynamicProperty(
25
+ legacyStoreKey,
26
+ legacyHookedKey,
27
+ component,
28
+ "userIndex",
29
+ onDependencyChange,
30
+ );
31
+
32
+ component.userIndex = 1;
33
+ await flushFrames();
34
+
35
+ expect(onDependencyChange).toHaveBeenCalled();
36
+ });
37
+
38
+ it("ne déclenche pas sans changement de valeur", async () => {
39
+ const component = { userIndex: 0 };
40
+ const onDependencyChange = vi.fn();
41
+
42
+ observeDynamicProperty(
43
+ legacyStoreKey,
44
+ legacyHookedKey,
45
+ component,
46
+ "userIndex",
47
+ onDependencyChange,
48
+ );
49
+
50
+ await flushFrames();
51
+
52
+ expect(onDependencyChange).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("propage un changement effectué dans le handler (même frame)", async () => {
56
+ const component = { a: 0, b: 0 };
57
+ const onAChange = vi.fn(() => {
58
+ component.b = 1;
59
+ });
60
+ const onBChange = vi.fn();
61
+
62
+ observeDynamicProperty(
63
+ legacyStoreKey,
64
+ legacyHookedKey,
65
+ component,
66
+ "a",
67
+ onAChange,
68
+ );
69
+ observeDynamicProperty(
70
+ legacyStoreKey,
71
+ legacyHookedKey,
72
+ component,
73
+ "b",
74
+ onBChange,
75
+ );
76
+
77
+ component.a = 1;
78
+ await flushFrames();
79
+
80
+ expect(onAChange).toHaveBeenCalled();
81
+ expect(onBChange).toHaveBeenCalled();
82
+ });
83
+
84
+ it("désinscription : plus de handler après cleanup", async () => {
85
+ const component = { userIndex: 0 };
86
+ const onDependencyChange = vi.fn();
87
+
88
+ const unobserve = observeDynamicProperty(
89
+ legacyStoreKey,
90
+ legacyHookedKey,
91
+ component,
92
+ "userIndex",
93
+ onDependencyChange,
94
+ );
95
+
96
+ unobserve();
97
+ component.userIndex = 2;
98
+ await flushFrames();
99
+
100
+ expect(onDependencyChange).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it("alerte si boucle infinie (handler qui remute la même prop)", async () => {
104
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
105
+ const component = { loop: 0 };
106
+ const onDependencyChange = vi.fn(() => {
107
+ component.loop += 1;
108
+ });
109
+
110
+ observeDynamicProperty(
111
+ legacyStoreKey,
112
+ legacyHookedKey,
113
+ component,
114
+ "loop",
115
+ onDependencyChange,
116
+ );
117
+
118
+ component.loop = 1;
119
+ await flushFrames(3);
120
+
121
+ expect(warn).toHaveBeenCalledWith(
122
+ expect.stringContaining("boucle infinie"),
123
+ );
124
+ });
125
+ });