@supersoniks/concorde 3.1.88 → 3.1.90

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.
@@ -1,14 +1,7 @@
1
- import { animate, fadeIn, fadeOut, Options } from "@lit-labs/motion";
2
1
  import { customScroll } from "@supersoniks/concorde/core/components/ui/_css/scroll";
3
2
  import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber";
4
3
  import { css, html, LitElement, nothing, PropertyValues } from "lit";
5
- import {
6
- customElement,
7
- property,
8
- query,
9
- queryAssignedElements,
10
- state,
11
- } from "lit/decorators.js";
4
+ import { customElement, property, query, state } from "lit/decorators.js";
12
5
  import { styleMap } from "lit/directives/style-map.js";
13
6
 
14
7
  import "@supersoniks/concorde/core/components/ui/modal/modal-actions";
@@ -56,70 +49,57 @@ export class Modal extends Subscriber(LitElement) {
56
49
  customScroll,
57
50
  css`
58
51
  :host {
59
- --sc-modal-py: 2.5rem;
60
- --sc-modal-px: 1.5rem;
61
- --sc-modal-max-w: min(100vw, 40rem);
62
- --sc-modal-max-h: 85vh;
63
- --sc-modal-rounded: var(--sc-rounded-lg);
64
- --sc-modal-z-index: 990;
52
+ --sc_py: 2.5rem;
53
+ --sc_px: 1.5rem;
54
+ --sc_max-w: min(100vw, 40rem);
55
+ --sc_max-h: 85vh;
56
+ --sc_rounded: var(--sc-rounded-lg);
57
+ --sc_z-index: 990;
58
+ --sc_backdrop-opacity: 0;
65
59
  }
66
60
 
67
61
  * {
68
62
  box-sizing: border-box;
69
63
  }
70
64
 
71
- .modal-wrapper {
72
- position: fixed;
73
- bottom: 0;
74
- left: 0;
75
- width: 100%;
76
- z-index: var(--sc-modal-z-index);
77
- align-items: center;
78
- justify-content: center;
79
- flex-direction: column;
80
- display: flex;
81
- pointer-events: none;
82
- }
83
-
84
- .modal-content {
85
- display: flex;
86
- flex-direction: column;
87
- min-height: 10rem;
88
- line-height: 1.25;
89
- }
90
-
91
- .modal {
65
+ #modal {
92
66
  background: var(--sc-base, #fff);
93
67
  color: var(--sc-base-content, #000);
94
68
  width: 100%;
95
69
  box-shadow: var(--sc-shadow-lg);
96
- border-radius: var(--sc-modal-rounded) var(--sc-modal-rounded) 0 0;
97
- pointer-events: auto;
98
- /*overflow: hidden;*/
70
+ border-radius: var(--sc_rounded) var(--sc_rounded) 0 0;
99
71
  transform: translateZ(0);
72
+ border: none;
73
+ outline: none;
74
+ height: fit-content;
75
+ padding: 0;
76
+ pointer-events: none;
77
+ transition: none !important;
78
+ opacity: 0;
79
+ position: fixed;
80
+ inset: 0;
81
+ z-index: var(--sc_z-index);
100
82
  }
101
83
 
102
- .overlay {
84
+ /*#modal::backdrop {*/
85
+ #backdrop {
86
+ opacity: var(--sc_backdrop-opacity, 0);
103
87
  background: var(
104
- --sc-modal-overlay-bg,
88
+ --sc_overlay-bg,
105
89
  var(--sc-base-200, rgba(0, 0, 0, 0.12))
106
90
  );
107
- left: 0;
108
- top: 0;
109
- right: 0;
110
- bottom: 0;
111
- z-index: var(--sc-modal-z-index);
112
- opacity: 0.8;
91
+ transition: opacity 0.2s ease-in-out;
92
+ inset: 0;
113
93
  position: fixed;
94
+ z-index: var(--sc_z-index);
114
95
  }
115
96
 
116
- ::slotted(sonic-modal-title),
117
- sonic-modal-title {
118
- margin-bottom: 1.25rem;
119
- }
120
- :host([align="left"]) ::slotted(sonic-modal-title),
121
- :host([align="left"]) sonic-modal-title {
122
- padding-right: 1em;
97
+ #modal-content {
98
+ display: flex;
99
+ flex-direction: column;
100
+ min-height: 10rem;
101
+ line-height: 1.25;
102
+ padding: var(--sc_py) var(--sc_px);
123
103
  }
124
104
 
125
105
  ::slotted(sonic-modal-subtitle),
@@ -129,44 +109,29 @@ export class Modal extends Subscriber(LitElement) {
129
109
  }
130
110
 
131
111
  @media (max-width: 767.5px) {
132
- .modal-wrapper,
133
- .modal {
112
+ #modal {
134
113
  max-width: none !important;
114
+ margin: auto 0 0 !important;
135
115
  width: 100% !important;
136
- border-radius: var(--sc-modal-rounded) var(--sc-modal-rounded) 0 0 !important;
116
+ border-radius: var(--sc_rounded) var(--sc_rounded) 0 0 !important;
137
117
  }
138
118
  }
119
+ :host([fullScreen]) #modal {
120
+ width: 100% !important;
121
+ height: 100% !important;
122
+ max-width: none !important;
123
+ max-height: none !important;
124
+ transform: none !important;
125
+ }
139
126
 
140
- @media (min-width: 768px) {
141
- .modal-wrapper {
142
- top: 50%;
143
- left: 50%;
144
- bottom: auto;
145
- right: auto;
146
- transform: translateX(-50%) translateY(-50%);
147
- }
148
-
149
- /* Solution améliorée pour les navigateurs supportant round() */
150
- @supports (transform: translateX(round(-50%, 1px))) {
151
- .modal-wrapper {
152
- transform: translateX(round(-50%, 1px)) translateY(round(-50%, 1px));
153
- }
154
- }
155
-
156
- :host([fullScreen]) .modal-wrapper {
157
- top: 0;
158
- left: 0;
159
- right: 0;
160
- bottom: 0;
161
- transform: none;
162
- }
163
-
164
- .modal {
165
- top: 50%;
166
- bottom: auto;
167
- right: auto;
168
- border-radius: var(--sc-modal-rounded);
169
- }
127
+ /* Layout des éléments slottés */
128
+ ::slotted(sonic-modal-title),
129
+ sonic-modal-title {
130
+ margin-bottom: 1.25rem;
131
+ }
132
+ :host([align="left"]) ::slotted(sonic-modal-title),
133
+ :host([align="left"]) sonic-modal-title {
134
+ padding-right: 1em;
170
135
  }
171
136
 
172
137
  :host([align="left"]) .modal-content {
@@ -184,6 +149,27 @@ export class Modal extends Subscriber(LitElement) {
184
149
  align-items: flex-end;
185
150
  }
186
151
 
152
+ /* Close button */
153
+ ::slotted(sonic-modal-close),
154
+ sonic-modal-close {
155
+ position: sticky;
156
+ display: block;
157
+ align-self: flex-end;
158
+ height: 0;
159
+ top: 0.5rem;
160
+ right: 0.5rem;
161
+ transform: translateX(calc(var(--sc_px) - 0.2rem))
162
+ translateY(calc(-1 * var(--sc_py) + 0.2rem));
163
+ z-index: 20;
164
+ }
165
+ :host([align="right"]) ::slotted(sonic-modal-close),
166
+ :host([align="right"]) sonic-modal-close {
167
+ right: auto;
168
+ left: 0.5rem;
169
+ transform: translateX(calc(-1 * var(--sc_px)))
170
+ translateY(calc(-1 * var(--sc_py)));
171
+ }
172
+
187
173
  /* Border radius */
188
174
  :host([rounded="none"]) modal {
189
175
  --sc-img-radius: 0 !important;
@@ -191,7 +177,6 @@ export class Modal extends Subscriber(LitElement) {
191
177
  `,
192
178
  ];
193
179
  static modals: Array<Modal> = [];
194
-
195
180
  @property({ type: Boolean }) forceAction = false;
196
181
  @property({ type: Boolean }) removeOnHide = false;
197
182
  @property({ type: Boolean }) removeHashOnHide = false;
@@ -200,28 +185,30 @@ export class Modal extends Subscriber(LitElement) {
200
185
  | "right"
201
186
  | "left" = "left";
202
187
 
203
- @property({ type: String }) padding = "var(--sc-modal-py) var(--sc-modal-px)";
204
- @property({ type: String }) maxWidth = "var(--sc-modal-max-w) ";
205
- @property({ type: String }) maxHeight = "var(--sc-modal-max-h) ";
188
+ @property({ type: String }) padding = "var(--sc_py) var(--sc_px)";
189
+ @property({ type: String }) maxWidth = "var(--sc_max-w)";
190
+ @property({ type: String }) maxHeight = "var(--sc_max-h)";
206
191
  @property({ type: String }) width = "100%";
207
- @property({ type: String }) height = "auto";
208
- @property({ type: String }) zIndex = "var(--sc-modal-z-index)";
209
- @property({ type: String }) effect?: effectType;
192
+ @property({ type: String }) height = "fit-content";
193
+ @property({ type: String }) zIndex = "var(--sc_z-index)";
194
+ @property({ type: String }) effect: effectType = "slide";
195
+
210
196
  @property({ type: Object }) options?: ModalCreateOptions;
197
+
211
198
  @property({ type: Boolean, reflect: true }) fullScreen = false;
212
199
  @property({ type: Boolean, reflect: true }) visible = false;
213
200
 
214
- @property({ type: Object }) animation?: Options;
215
-
216
- @query(".modal-wrapper") modalWrapper!: HTMLDivElement;
217
- @query(".modal") modalElement!: HTMLDivElement;
201
+ @query("#modal") _modalElement!: HTMLDialogElement;
218
202
 
219
203
  @property({ type: Boolean }) closeOnLocationChange = false;
220
204
  @state() location = "";
221
205
 
222
- @queryAssignedElements({ selector: "sonic-modal-close" })
223
- closeBtn!: Array<HTMLElement>;
224
- static create(options: ModalCreateOptions): Modal {
206
+ @state() private _animationState: "visible" | "in" | "out" | "hidden" =
207
+ "hidden";
208
+
209
+ _animationDuration = 400;
210
+
211
+ static async create(options: ModalCreateOptions): Promise<Modal> {
225
212
  const modal = document.createElement(tagName) as Modal;
226
213
  // modal styles
227
214
  modal.options = options;
@@ -241,16 +228,15 @@ export class Modal extends Subscriber(LitElement) {
241
228
  if (options.fullScreen) modal.fullScreen = options?.fullScreen;
242
229
  if (options.effect) modal.effect = options?.effect;
243
230
 
244
- if (options.paddingX)
245
- modal.style.setProperty("--sc-modal-px", options?.paddingX);
246
- if (options.paddingY)
247
- modal.style.setProperty("--sc-modal-py", options?.paddingY);
231
+ if (options.paddingX) modal.style.setProperty("--sc_px", options?.paddingX);
232
+ if (options.paddingY) modal.style.setProperty("--sc_py", options?.paddingY);
248
233
  if (options.zIndex)
249
- modal.style.setProperty("--sc-modal-z-index", options?.zIndex);
234
+ modal.style.setProperty("--sc_z-index", options?.zIndex);
250
235
 
251
236
  const container = Theme.getPopContainer();
252
237
  container.appendChild(modal);
253
- modal.show();
238
+ await modal.updateComplete;
239
+ await modal.show();
254
240
  return modal;
255
241
  }
256
242
 
@@ -258,31 +244,21 @@ export class Modal extends Subscriber(LitElement) {
258
244
  Modal.modals.push(this);
259
245
  LocationHandler.onChange(this);
260
246
  super.connectedCallback();
261
- this.handleFullsceen();
262
247
  }
248
+
263
249
  disconnectedCallback(): void {
264
250
  LocationHandler.offChange(this);
265
251
  Modal.modals.splice(Modal.modals.indexOf(this), 1);
252
+ this.removeEventListener("keydown", this.handleEscape);
266
253
  super.disconnectedCallback();
267
254
  }
268
255
 
269
- updated(): void {
270
- const currentModal = this as Modal;
271
-
272
- document.addEventListener("keydown", this.handleEscape);
273
-
274
- currentModal.closeBtn.forEach((closeBtn) => {
275
- closeBtn.addEventListener(
276
- "click",
277
- function () {
278
- currentModal.hide();
279
- },
280
- { once: true }
281
- );
282
- });
256
+ firstUpdated(): void {
257
+ this.addEventListener("keydown", this.handleEscape);
283
258
  }
284
259
 
285
260
  willUpdate(_changedProperties: PropertyValues): void {
261
+ // CLOSE modal on location change
286
262
  if (this.closeOnLocationChange && _changedProperties.has("location")) {
287
263
  const previousLocation = _changedProperties.get("location");
288
264
  if (
@@ -296,104 +272,80 @@ export class Modal extends Subscriber(LitElement) {
296
272
  }
297
273
  }
298
274
 
299
- if (_changedProperties.has("fullScreen")) {
300
- this.handleFullsceen();
275
+ super.willUpdate(_changedProperties);
276
+ }
277
+
278
+ protected updated(_changedProperties: PropertyValues): void {
279
+ const changedToVisible = !_changedProperties.get("visible") && this.visible;
280
+ const changedToHidden = _changedProperties.get("visible") && !this.visible;
281
+ if (changedToVisible && this._animationState === "hidden") {
282
+ this.show();
283
+ } else if (changedToHidden && this._animationState === "visible") {
284
+ this.hide();
301
285
  }
302
- if (_changedProperties.has("effect")) {
303
- if (this.effect == "fade") {
304
- this.animation = {
305
- keyframeOptions: {
306
- duration: 400,
307
- },
308
- in: fadeIn,
309
- out: fadeOut,
310
- };
311
- } else if (this.effect == "none") {
312
- this.animation = undefined;
313
- } else {
314
- this.animation = {
315
- keyframeOptions: {
316
- duration: 400,
317
- easing: `cubic-bezier(0.250, 0.250, 0.420, 1.225)`,
318
- },
319
- in: [
320
- {
321
- transform: "translateY(25%) scale(1)",
322
- boxShadow: "0 0 0 rgba(0,0,0,0)",
323
- opacity: 0,
324
- },
325
- ],
326
- out: [
327
- {
328
- transform: "translateY(20%) scale(1)",
329
- boxShadow: "0 0 0 rgba(0,0,0,0)",
330
- opacity: 0,
331
- },
332
- ],
333
- };
334
- }
286
+ }
287
+
288
+ // SI c'est en modal
289
+ // handleModalClick(e: MouseEvent): void {
290
+ // const isBackdropClick = e.target === e.currentTarget;
291
+ // if (
292
+ // isBackdropClick &&
293
+ // !this.forceAction &&
294
+ // this._animationState === "visible"
295
+ // ) {
296
+ // this.hide();
297
+ // }
298
+ // }
299
+
300
+ handleOverlayClick(_e: MouseEvent) {
301
+ if (!this.forceAction && this._animationState === "visible") {
302
+ this.hide();
335
303
  }
336
- super.willUpdate(_changedProperties);
337
304
  }
338
305
 
339
306
  render() {
340
- if (this.visible == false) return nothing;
341
-
342
307
  const modalStyles = {
343
- padding: this.padding,
344
308
  maxWidth: this.maxWidth,
345
309
  maxHeight: this.maxHeight,
346
310
  width: this.width,
347
311
  height: this.height,
348
312
  zIndex: this.zIndex,
349
- borderRadius: this.fullScreen ? "0" : "var(--sc-modal-rounded)",
313
+ borderRadius: this.fullScreen ? "0" : "var(--sc_rounded)",
314
+ pointerEvents: this._animationState === "visible" ? "auto" : "none",
350
315
  };
351
316
 
352
- const modalWrapperStyles = {
353
- maxWidth: this.maxWidth,
354
- maxHeight: this.maxHeight,
355
- width: this.width,
356
- height: this.height,
357
- borderRadius: this.fullScreen ? "0" : "var(--sc-modal-rounded)",
317
+ const modalContentStyles = {
318
+ padding: this.padding,
319
+ };
320
+
321
+ const overlayStyles = {
322
+ pointerEvents: this._animationState === "visible" ? "auto" : "none",
358
323
  };
359
324
 
360
325
  return html`
361
326
  <div
362
- class="overlay"
363
- @click="${!this.forceAction ? this.hide : null}"
364
- ${animate({
365
- keyframeOptions: {
366
- duration: 500,
367
- },
368
- in: fadeIn,
369
- out: [{ opacity: 0, pointerEvents: "none" }],
370
- })}
327
+ id="backdrop"
328
+ @click=${this.handleOverlayClick}
329
+ style=${styleMap(overlayStyles)}
371
330
  ></div>
372
- <div
373
- class="modal-wrapper"
374
- style=${styleMap(modalWrapperStyles)}
375
- ${animate({
376
- out: fadeOut,
377
- })}
378
- tabindex="0"
331
+ <dialog
332
+ id="modal"
333
+ part="modal"
334
+ class="custom-scroll"
335
+ aria-modal="true"
336
+ style=${styleMap(modalStyles)}
379
337
  >
380
- <div
381
- part="modal"
382
- class="modal custom-scroll"
383
- style=${styleMap(modalStyles)}
384
- ${animate(this.animation)}
385
- >
386
- <div class="modal-content">
387
- ${!this.forceAction
388
- ? html`<sonic-modal-close></sonic-modal-close>`
389
- : nothing}
390
- ${this.modalFragment("title")} ${this.modalFragment("subtitle")}
391
- ${this.modalFragment("content")} ${this.modalFragment("actions")}
392
-
393
- <slot></slot>
394
- </div>
395
- </div>
396
- </div>
338
+ ${this._animationState !== "hidden"
339
+ ? html`<div id="modal-content" style=${styleMap(modalContentStyles)}>
340
+ ${!this.forceAction
341
+ ? html`<sonic-modal-close></sonic-modal-close>`
342
+ : nothing}
343
+ ${this.modalFragment("title")} ${this.modalFragment("subtitle")}
344
+ ${this.modalFragment("content")} ${this.modalFragment("actions")}
345
+ <slot></slot>
346
+ </div>`
347
+ : nothing}
348
+ </dialog>
397
349
  `;
398
350
  }
399
351
 
@@ -409,7 +361,6 @@ export class Modal extends Subscriber(LitElement) {
409
361
  } else {
410
362
  output = unsafeHTML(optionValue as string);
411
363
  }
412
-
413
364
  switch (optionKey) {
414
365
  case "title":
415
366
  return html`<sonic-modal-title>${output}</sonic-modal-title>`;
@@ -424,26 +375,32 @@ export class Modal extends Subscriber(LitElement) {
424
375
  }
425
376
  }
426
377
 
427
- show(): void {
428
- this.visible = true;
429
- this.modalElement?.setAttribute("tabindex", "0");
430
- this.modalElement?.focus();
431
- this.dispatchEvent(new CustomEvent("show"));
378
+ // Show the modal
379
+ async show(): Promise<void> {
380
+ this._modalElement.show();
381
+ this._animationState = "in";
382
+ this.animateIn(this._modalElement, this.effect).then(() => {
383
+ this._animationState = "visible";
384
+ this.visible = true;
385
+ this.dispatchEvent(new CustomEvent("show"));
386
+ this._modalElement.focus();
387
+ });
432
388
  }
433
389
 
434
- hide(): Promise<void> {
435
- return new Promise((resolve) => {
436
- this.visible = false;
437
- this.modalElement?.setAttribute("tabindex", "-1");
438
- this.dispatchEvent(new CustomEvent("hide"));
439
-
440
- if (this.hasAttribute("resetDataProviderOnHide")) {
441
- PublisherManager.get(this.getAttribute("resetDataProviderOnHide")).set(
442
- {}
443
- );
444
- }
445
-
446
- setTimeout(() => {
390
+ // Hide the modal
391
+ async hide(): Promise<void> {
392
+ this._animationState = "out";
393
+ this.dispatchEvent(new CustomEvent("hide"));
394
+ await Promise.all([this.animateOut(this._modalElement, this.effect)]).then(
395
+ () => {
396
+ this._modalElement.close();
397
+ this._animationState = "hidden";
398
+ this.visible = false;
399
+ if (this.hasAttribute("resetDataProviderOnHide")) {
400
+ PublisherManager.get(
401
+ this.getAttribute("resetDataProviderOnHide")
402
+ ).set({});
403
+ }
447
404
  if (this.removeHashOnHide) {
448
405
  window.history.replaceState({}, "", window.location.pathname);
449
406
  }
@@ -451,39 +408,114 @@ export class Modal extends Subscriber(LitElement) {
451
408
  this.remove();
452
409
  }
453
410
  this.dispatchEvent(new CustomEvent("hidden"));
454
- resolve();
455
- }, 480);
456
- });
411
+ }
412
+ );
457
413
  }
458
414
 
415
+ // Hide and remove the modal
459
416
  dispose() {
460
417
  this.hide();
461
418
  this.remove();
462
419
  }
420
+
421
+ // Hide and remove all modals
463
422
  static disposeAll() {
464
423
  Modal.modals.forEach((modal) => {
465
424
  modal.dispose();
466
425
  });
467
426
  }
468
427
 
428
+ // Hide the last visible modal
469
429
  handleEscape(e: KeyboardEvent): void {
470
430
  if (e.key === "Escape") {
471
- Modal.modals.forEach((modal: Modal) => {
472
- if (!modal.forceAction) {
473
- modal.hide();
474
- }
475
- });
431
+ e.preventDefault();
432
+ const visibleModals = Modal.modals.filter(
433
+ (modal) => modal._animationState === "visible" && !modal.forceAction
434
+ );
435
+ if (visibleModals.length > 0) {
436
+ const lastVisibleModal = visibleModals[visibleModals.length - 1];
437
+ lastVisibleModal.hide();
438
+ }
476
439
  }
477
440
  }
478
441
 
479
- handleFullsceen(): void {
480
- // set fullscreen
481
- if (this.fullScreen) {
482
- this.width = "100%";
483
- this.height = "100%";
484
- this.maxWidth = "none";
485
- this.maxHeight = "none";
486
- }
442
+ // ------------------------
443
+ // ANIMATIONS
444
+ // ------------------------
445
+ private animateIn(
446
+ element: HTMLElement,
447
+ effect: effectType = "slide"
448
+ ): Promise<void> {
449
+ return new Promise((resolve) => {
450
+ if (!element) return resolve();
451
+
452
+ this.style.setProperty("--sc_backdrop-opacity", "0.8");
453
+ let animation: Animation;
454
+ if (effect === "slide") {
455
+ animation = element.animate(
456
+ [
457
+ {
458
+ opacity: 0,
459
+ transform: "translateY(3.25rem)",
460
+ },
461
+ {
462
+ opacity: 1,
463
+ transform: "translateY(0)",
464
+ },
465
+ ],
466
+ {
467
+ duration: this._animationDuration,
468
+ easing: "cubic-bezier(0.250, 0.250, 0.420, 1.225)",
469
+ fill: "forwards",
470
+ delay: 80,
471
+ }
472
+ );
473
+ } else {
474
+ // fade (ou none)
475
+ animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
476
+ duration: this._animationDuration,
477
+ fill: "forwards",
478
+ });
479
+ }
480
+ animation.onfinish = () => resolve();
481
+ });
482
+ }
483
+
484
+ private animateOut(
485
+ element: HTMLElement,
486
+ effect: effectType = "slide"
487
+ ): Promise<void> {
488
+ return new Promise((resolve) => {
489
+ if (!element) return resolve();
490
+ this.style.setProperty("--sc_backdrop-opacity", "0");
491
+ let animation: Animation;
492
+ if (effect === "slide") {
493
+ animation = element.animate(
494
+ [
495
+ {
496
+ opacity: 1,
497
+ transform: "translateY(0)",
498
+ },
499
+ {
500
+ opacity: 0,
501
+ transform: "translateY(3.25rem)",
502
+ },
503
+ ],
504
+ {
505
+ duration: this._animationDuration,
506
+ easing: "cubic-bezier(0.250, 0.250, 0.420, 1.225)",
507
+ fill: "forwards",
508
+ }
509
+ );
510
+ } else {
511
+ // fade (ou none)
512
+ animation = element.animate([{ opacity: 1 }, { opacity: 0 }], {
513
+ duration: this._animationDuration,
514
+ fill: "forwards",
515
+ });
516
+ }
517
+ animation.onfinish = () => resolve();
518
+ });
487
519
  }
488
520
  }
489
521
 
File without changes