@ktortu/aaa 0.9.9 → 0.10.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.
package/README.md CHANGED
@@ -40,7 +40,7 @@ export class Exemple {
40
40
  ```
41
41
 
42
42
  Points d'entrée TypeScript : `@ktortu/aaa/button`, `/card`, `/dialog`, `/layout`, `/menu`, `/snackbar`, `/tabs`,
43
- `/tooltip`, `/forms`, `/cdk` (breakpoints, viewport, sheet-drag, id-generator) et `/i18n`
43
+ `/tooltip`, `/forms`, `/icon` (icônes), `/cdk` (breakpoints, viewport, sheet-drag, id-generator) et `/i18n`
44
44
  (traductions). La racine `@ktortu/aaa` ré-exporte tout par commodité, mais importer depuis le
45
45
  point d'entrée précis préserve le tree-shaking. Les thèmes sont des fichiers CSS (cf. §Styles),
46
46
  pas un point d'entrée TypeScript.
@@ -90,15 +90,49 @@ type `Partial<…>`) ou via les helpers `provideKt*` :
90
90
  ```ts
91
91
  import { KT_BUTTON_CONFIG } from '@ktortu/aaa/button';
92
92
  import { provideKtBreakpoints } from '@ktortu/aaa/cdk';
93
- import { provideKtDialogDefaults } from '@ktortu/aaa/dialog';
93
+ import { provideKtDialog, provideKtDialogDefaults } from '@ktortu/aaa/dialog';
94
94
 
95
95
  providers: [
96
96
  { provide: KT_BUTTON_CONFIG, useValue: { size: 'lg' } },
97
97
  provideKtBreakpoints({ tablet: 768, desktop: 1200 }),
98
- provideKtDialogDefaults({ maxWidth: '40rem' }),
98
+ provideKtDialogDefaults({ maxWidth: '40rem' }), // config CDK du dialog
99
+ provideKtDialog({ sheetCloseButton: true }), // options maison du dialog
99
100
  ];
100
101
  ```
101
102
 
103
+ Le dialog a **deux** providers, complémentaires et sans recouvrement :
104
+ `provideKtDialogDefaults()` porte la config `@angular/cdk/dialog` (plancher a11y, largeurs,
105
+ classes) ; `provideKtDialog()` porte les options propres à la lib (`KT_DIALOG_CONFIG`).
106
+
107
+ #### Bouton de fermeture des bottom-sheets
108
+
109
+ `sheetCloseButton` ajoute une croix auto-rendue en haut de la carte, hors flux (cible tactile
110
+ 44 px, WCAG 2.5.5). **Désactivée par défaut** — pour une raison de compatibilité, pas d'ergonomie.
111
+
112
+ `[ktDialogHeader]` ne rend aucune croix : c'est une simple rangée flex, où l'on POSE la sienne
113
+ (`<button ktButton iconOnly icon="close" ktDialogClose>`). Mais comme c'est la composition usuelle,
114
+ activer l'option par défaut ferait apparaître une **deuxième** croix chez ces consommateurs-là sans
115
+ qu'ils aient rien changé. Le conteneur avertit en mode dev si les deux coexistent hors barre
116
+ d'actions.
117
+
118
+ Recommandée, donc, sur toute sheet qui ne pose pas déjà son propre bouton de fermeture en tête.
119
+
120
+ Surchargeable par ouverture, comme `presentation` :
121
+
122
+ ```ts
123
+ export const injectShareSheet = () =>
124
+ shareSheet.injectOpener(ShareSheet, { presentation: 'sheet', sheetCloseButton: true });
125
+ ```
126
+
127
+ La position s'ajuste **en CSS seul** (propriétés logiques, donc correctes en RTL) :
128
+ `--dialog-close-inset-block-start`, `--dialog-close-inset-inline-end`, `--dialog-close-size`,
129
+ `--dialog-close-icon-size`, `--dialog-close-color`, `--dialog-close-bg`, `--dialog-close-glyph`.
130
+ Chacune retombe sur une couche partagée `--kt-sheet-close-*`, pour harmoniser d'un coup ce bouton
131
+ et celui du Select compact.
132
+
133
+ L'option `sheetHandle: false` retire la poignée décorative (ADR-0005). Elle remplace le
134
+ `panelClass` `kt-dialog--no-handle`, **déprécié** mais toujours honoré.
135
+
102
136
  ## Styles (CSS)
103
137
 
104
138
  L'apparence vit dans des **feuilles CSS globales** thémées par des tokens `--kt-*` : importez le
@@ -169,6 +203,7 @@ Bundles disponibles :
169
203
  | `@ktortu/aaa/snackbar.css` | snackbar (requiert l'overlay CDK + `a11y-prebuilt.css`) |
170
204
  | `@ktortu/aaa/forms.css` | base des formulaires (champs, chips, switch) |
171
205
  | `@ktortu/aaa/tabs.css` | onglets |
206
+ | `@ktortu/aaa/icon.css` | icônes (`ktIcon`) |
172
207
 
173
208
  > `foundation.css` doit **toujours** être importé en premier (les bundles composant en dérivent).
174
209
  > Si vous utilisez plusieurs composants, l'agrégat `@ktortu/aaa/styles.css` fait tout cela dans le
@@ -177,6 +212,8 @@ Bundles disponibles :
177
212
  > Les formulaires « riches » (Select, MultiSelect, Field, Chips) embarquent leur CSS via les
178
213
  > composants eux-mêmes (`styleUrl`) : rien à importer en plus pour ceux-ci.
179
214
 
215
+ > **Note icônes remplies** : L'utilisation de `[fill]` ou `fill` sur un `ktIcon` exige de charger une police dont l'axe `FILL` est ajustable (ex. Material Symbols avec l'axe dans la requête URL).
216
+
180
217
  ## Développement
181
218
 
182
219
  ```bash
package/dialog/dialog.css CHANGED
@@ -31,6 +31,22 @@
31
31
  --dialog-focus-ring-color: var(--kt-focus-ring-color, #0842a0);
32
32
  --dialog-focus-ring-width: var(--kt-focus-ring-width, 2px);
33
33
 
34
+ /* ============ Bouton de fermeture auto-rendu de la bottom-sheet ============ */
35
+ /* Rendu seulement si l'option `sheetCloseButton` est active (cf. KtDialogConfig). Chaque token
36
+ retombe sur une couche PARTAGÉE --kt-sheet-close-* (même mécanisme que --dialog-radius →
37
+ --kt-sheet-radius) : un thème harmonise d'un coup ce bouton et celui du Select compact.
38
+ La cible fait 44px RÉELS (WCAG 2.5.5 AAA) pour un glyphe de 1.25rem — l'anneau de focus
39
+ épouse donc la cible, comme sur le Select. */
40
+ --dialog-close-size: var(--kt-sheet-close-size, 44px);
41
+ --dialog-close-icon-size: var(--kt-sheet-close-icon-size, 1.25rem);
42
+ --dialog-close-inset-block-start: var(--kt-sheet-close-inset-block-start, 0.25rem);
43
+ --dialog-close-inset-inline-end: var(--kt-sheet-close-inset-inline-end, 0.25rem);
44
+ --dialog-close-color: var(--kt-sheet-close-color, var(--dialog-muted));
45
+ /* Bascules NON déclarées exprès (défauts entre parenthèses) :
46
+ --dialog-close-bg (transparent) : pastille opaque quand la croix flotte au-dessus d'un contenu
47
+ à fond propre (image, média) — sans elle, le contraste 1.4.11 n'est pas garantissable.
48
+ --dialog-close-glyph ('close') : ligature du glyphe. */
49
+
34
50
  /* ============ Gouttière du conteneur scrollable ============ */
35
51
  /* Le dialog a des coins arrondis + une barre d'actions sticky : la gouttière à TRACK PLEIN
36
52
  héritée de la page y dessine un bandeau vertical disgracieux le long de l'arrondi. On force
@@ -241,6 +257,14 @@
241
257
  /* Le scrim translucide disparaît en forced-colors : on garde une séparation visible. */
242
258
  background: rgb(0 0 0 / 50%);
243
259
  }
260
+ /* Le fond transparent et la couleur atténuée du bouton de fermeture sont écrasés par le
261
+ système : on le rattache aux couleurs de contrôle pour qu'il reste identifiable. */
262
+ .kt-dialog-container__sheet-close {
263
+ color: ButtonText;
264
+ }
265
+ .kt-dialog-container__sheet-close:focus-visible {
266
+ outline-color: Highlight;
267
+ }
244
268
  }
245
269
 
246
270
  /* ====================================================================================
@@ -351,6 +375,10 @@
351
375
 
352
376
  /* La carte : reprend la surface du dialog (déplacée du conteneur, devenu scroller). */
353
377
  .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__layout {
378
+ /* Référentiel du bouton de fermeture absolu. IMPÉRATIF que ce soit la CARTE et non le
379
+ conteneur : celui-ci est le scroller à snap, un ancrage dessus suivrait le geste et sortirait
380
+ de l'écran au premier glissement. */
381
+ position: relative;
354
382
  scroll-snap-align: end;
355
383
  scroll-snap-stop: always;
356
384
  display: flex;
@@ -399,6 +427,63 @@
399
427
  background: var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5));
400
428
  }
401
429
 
430
+ /* ===== Bouton de fermeture auto-rendu (option `sheetCloseButton`) =====
431
+ Hors flux : ancré en absolu sur la carte, il ne décale RIEN (zéro layout shift) et n'impose
432
+ aucune contrainte au markup projeté. Pas de `touch-action` : le glissement doit rester
433
+ saisissable AU TRAVERS du bouton (ADR-0005 — la sheet s'attrape partout). Un vrai glissement
434
+ n'émet pas de `click` (le navigateur le supprime après un défilement) : seul un tap ferme. */
435
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__sheet-close {
436
+ position: absolute;
437
+ inset-block-start: var(--dialog-close-inset-block-start);
438
+ inset-inline-end: var(--dialog-close-inset-inline-end);
439
+ /* Au-dessus d'un éventuel [ktDialogHeader] collant projeté par le consommateur (z-index 1). */
440
+ z-index: 2;
441
+ display: inline-flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ inline-size: var(--dialog-close-size);
445
+ block-size: var(--dialog-close-size);
446
+ padding: 0;
447
+ border: 0;
448
+ border-radius: 50%;
449
+ background: var(--dialog-close-bg, transparent);
450
+ color: var(--dialog-close-color);
451
+ cursor: pointer;
452
+ }
453
+
454
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__sheet-close:focus-visible {
455
+ outline: var(--dialog-focus-ring-width) solid var(--dialog-focus-ring-color);
456
+ outline-offset: 2px;
457
+ }
458
+
459
+ .kt-dialog-container__sheet-close-icon {
460
+ display: inline-grid;
461
+ place-items: center;
462
+ inline-size: var(--dialog-close-icon-size);
463
+ block-size: var(--dialog-close-icon-size);
464
+ overflow: hidden; /* Sécurité anti-débordement de ligature brute (cf. .kt-alert-dialog__icon) */
465
+ }
466
+
467
+ .kt-dialog-container__sheet-close-icon::before {
468
+ content: var(--dialog-close-glyph, 'close');
469
+ font-family: var(--kt-icon-font, 'Material Symbols Outlined');
470
+ font-weight: var(--kt-icon-font-weight, normal);
471
+ font-style: normal;
472
+ font-size: var(--dialog-close-icon-size);
473
+ line-height: 1;
474
+ font-feature-settings: 'liga';
475
+ -webkit-font-smoothing: antialiased;
476
+ }
477
+
478
+ /* Réserve horizontale sous la croix : sans elle, un titre long passerait DESSOUS. Ciblage
479
+ `> * >` (l'hôte du composant projeté est l'intermédiaire) = régions de PREMIER niveau seulement,
480
+ ce qui exclut volontairement un [ktDialogTitle] imbriqué dans un [ktDialogHeader] — celui-ci
481
+ garde son padding nul et c'est le header qui porte la réserve. */
482
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__layout--closable > * > [ktDialogTitle],
483
+ .cdk-overlay-pane.kt-dialog--sheet .kt-dialog-container__layout--closable > * > [ktDialogHeader] {
484
+ padding-inline-end: calc(var(--dialog-close-size) + var(--dialog-close-inset-inline-end));
485
+ }
486
+
402
487
  /* Mouvement réduit : pas de fondu (WCAG 2.3.3). La sheet gère le sien en JS (scroll instantané). */
403
488
  @media (prefers-reduced-motion: reduce) {
404
489
  .cdk-overlay-pane.kt-dialog--fullscreen .cdk-dialog-container {
@@ -1,8 +1,9 @@
1
1
  import { DialogRef, DEFAULT_DIALOG_CONFIG, CdkDialogContainer, Dialog, DIALOG_DATA } from '@angular/cdk/dialog';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, ElementRef, Renderer2, afterNextRender, Directive, input, isDevMode, PLATFORM_ID, DestroyRef, signal, ChangeDetectionStrategy, Component, ViewContainerRef, Injectable } from '@angular/core';
3
+ import { inject, ElementRef, Renderer2, afterNextRender, Directive, input, isDevMode, InjectionToken, PLATFORM_ID, DestroyRef, signal, ChangeDetectionStrategy, Component, ViewContainerRef, Injectable } from '@angular/core';
4
4
  import { KtIdGenerator, KtBodyScrollLock, KtViewport } from '@ktortu/aaa/cdk';
5
5
  import { isPlatformBrowser } from '@angular/common';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
7
  import * as i1 from '@angular/cdk/portal';
7
8
  import { PortalModule } from '@angular/cdk/portal';
8
9
  import { map } from 'rxjs/operators';
@@ -182,6 +183,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
182
183
  * aria-labelledby). En usage simple — titre nu sans header — [ktDialogTitle] conserve son propre
183
184
  * padding : ce header n'est à utiliser que pour les en-têtes composés (icône, close top-right…).
184
185
  *
186
+ * ⚠️ Ne rend RIEN par lui-même — pas de bouton de fermeture implicite : c'est vous qui posez la
187
+ * croix (cf. exemple). En présentation `sheet`, l'option `sheetCloseButton` en rend une
188
+ * automatiquement, hors flux ; les deux ensemble feraient doublon (le conteneur avertit en dev).
189
+ *
185
190
  * @example
186
191
  * ```html
187
192
  * <header ktDialogHeader>
@@ -256,7 +261,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
256
261
  }] });
257
262
  /**
258
263
  * @deprecated ADR-0005 — la poignée est désormais RENDUE AUTOMATIQUEMENT par `KtDialogContainer`
259
- * en présentation `sheet` (opt-out : panelClass additionnel `kt-dialog--no-handle`), et le
264
+ * en présentation `sheet` (opt-out : option typée `sheetHandle: false`), et le
260
265
  * drag-to-dismiss s'attrape sur TOUTE la surface de la sheet (scroll-snap natif). Cette
261
266
  * directive est INERTE et son hôte est masqué (évite une double poignée) : retirez-la de vos
262
267
  * templates. Sera supprimée dans une prochaine version majeure.
@@ -334,6 +339,41 @@ function provideKtDialogDefaults(overrides) {
334
339
  useValue: { ...KT_DIALOG_AAA_DEFAULTS, ...overrides },
335
340
  };
336
341
  }
342
+ /**
343
+ * Défauts effectifs des options maison du dialog. Objet `Required` figé : c'est la SOURCE des
344
+ * valeurs de repli, lue à la fois par l'ouvreur (résolution) et par le conteneur (fusion
345
+ * idempotente), et énumérée par la garde de complétude i18n.
346
+ */
347
+ const DEFAULT_KT_DIALOG_CONFIG = {
348
+ sheetCloseButton: false,
349
+ sheetCloseLabel: 'Close',
350
+ sheetHandle: true,
351
+ };
352
+ /**
353
+ * Token des options maison du dialog. Deux usages, un seul chemin de résolution :
354
+ * - **application** : fourni via {@link provideKtDialog} (valeur partielle) ;
355
+ * - **par ouverture** : `injectKtDialogOpener` REFOURNIT ce même token, déjà RÉSOLU, dans
356
+ * l'injecteur du conteneur (canal `container.providers` du CDK). Le conteneur fusionne
357
+ * toujours par-dessus {@link DEFAULT_KT_DIALOG_CONFIG} : l'opération est idempotente sur une
358
+ * valeur déjà résolue, et reste correcte si le conteneur est monté sans passer par l'ouvreur.
359
+ */
360
+ const KT_DIALOG_CONFIG = new InjectionToken('KT_DIALOG_CONFIG');
361
+ /**
362
+ * Enregistre les options maison du dialog pour l'application (ou un sous-arbre).
363
+ * Complémentaire de {@link provideKtDialogDefaults}, qui porte la config du CDK : les deux
364
+ * coexistent et ne se recouvrent pas.
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * // app.config.ts — bouton de fermeture sur toutes les bottom-sheets
369
+ * providers: [provideKtDialog({ sheetCloseButton: true })];
370
+ * ```
371
+ * @param config Options partielles fusionnées par-dessus `DEFAULT_KT_DIALOG_CONFIG`.
372
+ * @returns Un `Provider` pour le token `KT_DIALOG_CONFIG`.
373
+ */
374
+ function provideKtDialog(config) {
375
+ return { provide: KT_DIALOG_CONFIG, useValue: config };
376
+ }
337
377
  /** panelClass d'une présentation CONCRÈTE (sans variante responsive). */
338
378
  function concreteDialogPanelClass(p) {
339
379
  if (p === 'fullscreen')
@@ -367,6 +407,8 @@ function resolveKtDialogPanelClass(presentation = 'centered', compact = false) {
367
407
  return concreteDialogPanelClass(compact ? 'sheet' : 'centered'); // 'centered-sheet'
368
408
  }
369
409
 
410
+ /** panelClass d'opt-out historique de la poignée, conservé comme repli DÉPRÉCIÉ (cf. `sheetHandle`). */
411
+ const LEGACY_NO_HANDLE_CLASS = 'kt-dialog--no-handle';
370
412
  class KtDialogContainer extends CdkDialogContainer {
371
413
  dialogRef = inject(DialogRef);
372
414
  elementRef = inject(ElementRef);
@@ -374,6 +416,14 @@ class KtDialogContainer extends CdkDialogContainer {
374
416
  platformId = inject(PLATFORM_ID);
375
417
  destroyRef = inject(DestroyRef);
376
418
  bodyScrollLock = inject(KtBodyScrollLock);
419
+ /** Options maison RÉSOLUES. `injectKtDialogOpener` refournit `KT_DIALOG_CONFIG` déjà résolu dans
420
+ l'injecteur du conteneur : la fusion ci-dessous est alors un no-op. Elle reste indispensable
421
+ quand le conteneur est monté sans l'ouvreur (`dialog.open` brut), où le token remonte à
422
+ l'application et n'est que partiel. Un seul chemin de code, idempotent. */
423
+ ktConfig = {
424
+ ...DEFAULT_KT_DIALOG_CONFIG,
425
+ ...(inject(KT_DIALOG_CONFIG, { optional: true }) ?? {}),
426
+ };
377
427
  isClosing = signal(false, /* @ts-ignore */
378
428
  ...(ngDevMode ? [{ debugName: "isClosing" }] : /* istanbul ignore next */ []));
379
429
  // --- Sheet scroll-snap (ADR-0005) : détection du snap « fermé » + fermeture différée ---
@@ -419,14 +469,74 @@ class KtDialogContainer extends CdkDialogContainer {
419
469
  if (!hasName) {
420
470
  console.warn('[ktDialog] dialog sans nom accessible : ajoutez un [ktDialogTitle] (ou `aria-label` via la config d’ouverture) — WCAG 4.1.2.');
421
471
  }
472
+ this.warnOnConfigMisuse();
473
+ });
474
+ // Repli de FOCUS INITIAL. La config par défaut de la lib vise un SÉLECTEUR
475
+ // (`autoFocus: '[ktDialogFocusInitial]'`) ; quand aucun élément ne le porte, le CDK ne focalise
476
+ // RIEN (son repli sur le conteneur n'existe que pour les modes `first-tabbable`/`dialog`/false).
477
+ // Le focus reste alors sur le déclencheur, HORS du dialog, et Tab promène l'utilisateur dans la
478
+ // page derrière. On rattrape ici sur le conteneur (tabindex -1) — jamais sur un bouton :
479
+ // atterrir d'emblée sur « Fermer » est un anti-pattern lecteur d'écran (WCAG 2.4.3).
480
+ // `preventScroll` : en présentation sheet le conteneur EST le scroller à snap, focaliser ne doit
481
+ // pas perturber le scroll d'entrée en cours.
482
+ this._focusTrapped.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
483
+ if (this.host.contains(this.host.ownerDocument.activeElement))
484
+ return;
485
+ this.host.focus({ preventScroll: true });
422
486
  });
423
487
  }
488
+ /** Garde-fous dev sur la config maison : usage hors sheet, double bouton de fermeture, opt-out
489
+ déprécié de la poignée. On AVERTIT sans jamais corriger en silence — un masquage automatique
490
+ dépendrait de l'ordre de rendu du portail et serait indébogable. */
491
+ warnOnConfigMisuse() {
492
+ if (this.hasLegacyNoHandleClass()) {
493
+ console.warn(`[ktDialog] panelClass '${LEGACY_NO_HANDLE_CLASS}' déprécié : utilisez l’option typée ` +
494
+ '`sheetHandle: false` (à l’ouverture ou via provideKtDialog).');
495
+ }
496
+ if (this.ktConfig.sheetCloseButton && !this.isSheet()) {
497
+ console.warn('[ktDialog] `sheetCloseButton` ne s’applique qu’à la présentation `sheet` et reste sans effet ' +
498
+ 'ici : en centré / plein écran, posez le bouton dans un [ktDialogHeader].');
499
+ }
500
+ // Collision visuelle recherchée : une croix posée en TÊTE (typiquement dans un [ktDialogHeader]),
501
+ // qui doublerait celle du conteneur. Un [ktDialogClose] dans la barre d'ACTIONS est légitime
502
+ // (« Annuler », ou une action qui ferme au passage) et ne collisionne pas : on l'exclut.
503
+ // Heuristique volontairement partielle par ailleurs : elle voit la forme ATTRIBUT `ktDialogClose`,
504
+ // pas la forme liée `[ktDialogClose]="x"` (un binding de propriété ne reflète aucun attribut).
505
+ const strayClose = this.showSheetClose() &&
506
+ [...this.host.querySelectorAll('[ktDialogClose]')].some((el) => !el.closest('[ktDialogActions]'));
507
+ if (strayClose) {
508
+ console.warn('[ktDialog] double bouton de fermeture : `sheetCloseButton` est actif alors que le contenu ' +
509
+ 'porte déjà un [ktDialogClose] hors barre d’actions (en-tête ?). Gardez l’un OU l’autre.');
510
+ }
511
+ }
424
512
  isSheet() {
425
513
  return this._config.panelClass?.includes('kt-dialog--sheet') ?? false;
426
514
  }
427
- /** Poignée décorative auto-rendue en présentation sheet. Opt-out : panelClass `kt-dialog--no-handle`. */
515
+ /** panelClass d'opt-out historique de la poignée repli DÉPRÉCIÉ, remplacé par `sheetHandle`. */
516
+ hasLegacyNoHandleClass() {
517
+ return this._config.panelClass?.includes(LEGACY_NO_HANDLE_CLASS) ?? false;
518
+ }
519
+ /** Poignée décorative auto-rendue en présentation sheet (ADR-0005). Opt-out : option `sheetHandle`
520
+ (le panelClass historique reste honoré, en déprécié). */
428
521
  showSheetHandle() {
429
- return !this._config.panelClass?.includes('kt-dialog--no-handle');
522
+ if (this.hasLegacyNoHandleClass())
523
+ return false;
524
+ return this.ktConfig.sheetHandle;
525
+ }
526
+ /** Bouton de fermeture auto-rendu : opt-in `sheetCloseButton`, et UNIQUEMENT en présentation sheet
527
+ (ailleurs, la croix se pose à la main dans un `[ktDialogHeader]` — cf. `KtDialogConfig`). */
528
+ showSheetClose() {
529
+ return this.isSheet() && this.ktConfig.sheetCloseButton;
530
+ }
531
+ /** Clic sur le bouton de fermeture auto-rendu. Ne consulte VOLONTAIREMENT pas `disableClose` :
532
+ contrairement au tap-extérieur et à Échap (fermetures « par inadvertance » que `disableClose`
533
+ protège), ce bouton est une sortie EXPLICITEMENT demandée par le dev via `sheetCloseButton` —
534
+ le combiner à `disableClose` est le patron « une seule sortie contrôlée ». Le drapeau
535
+ `isClosing` garde l'idempotence face à un geste de glissement simultané. */
536
+ onSheetCloseClick() {
537
+ if (this.isClosing())
538
+ return;
539
+ this.dialogRef.close();
430
540
  }
431
541
  /** Tap sur le spacer (zone extérieure à la carte) = fermeture, façon backdrop. Le conteneur
432
542
  captant désormais le tactile (pointer-events auto, requis pour le drag iOS), le clic ne
@@ -560,10 +670,23 @@ class KtDialogContainer extends CdkDialogContainer {
560
670
  backdrop CDK, la fermeture au tap est donc portée ici (disableClose respecté). -->
561
671
  <div class="kt-dialog-container__spacer" aria-hidden="true" (click)="onScrimClick()"></div>
562
672
  }
563
- <div class="kt-dialog-container__layout">
673
+ <div class="kt-dialog-container__layout" [class.kt-dialog-container__layout--closable]="showSheetClose()">
674
+ @if (showSheetClose()) {
675
+ <!-- Bouton de fermeture AUTO-RENDU (opt-in 'sheetCloseButton'). Placé en TÊTE du document
676
+ pour ouvrir l'ordre de tabulation, mais ancré en absolu sur la carte (cf. dialog.css) :
677
+ il ne consomme pas de place dans la colonne. Aucun touch-action ici — le geste de
678
+ glissement doit rester saisissable au travers du bouton (ADR-0005). -->
679
+ <button
680
+ type="button"
681
+ class="kt-dialog-container__sheet-close"
682
+ [attr.aria-label]="ktConfig.sheetCloseLabel"
683
+ (click)="onSheetCloseClick()"
684
+ >
685
+ <span class="kt-dialog-container__sheet-close-icon" aria-hidden="true"></span>
686
+ </button>
687
+ }
564
688
  @if (isSheet() && showSheetHandle()) {
565
- <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out :
566
- panelClass additionnel 'kt-dialog--no-handle' à l'ouverture). -->
689
+ <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out : 'sheetHandle'). -->
567
690
  <div class="kt-dialog-container__sheet-handle" aria-hidden="true"></div>
568
691
  }
569
692
  <ng-template cdkPortalOutlet></ng-template>
@@ -597,10 +720,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
597
720
  backdrop CDK, la fermeture au tap est donc portée ici (disableClose respecté). -->
598
721
  <div class="kt-dialog-container__spacer" aria-hidden="true" (click)="onScrimClick()"></div>
599
722
  }
600
- <div class="kt-dialog-container__layout">
723
+ <div class="kt-dialog-container__layout" [class.kt-dialog-container__layout--closable]="showSheetClose()">
724
+ @if (showSheetClose()) {
725
+ <!-- Bouton de fermeture AUTO-RENDU (opt-in 'sheetCloseButton'). Placé en TÊTE du document
726
+ pour ouvrir l'ordre de tabulation, mais ancré en absolu sur la carte (cf. dialog.css) :
727
+ il ne consomme pas de place dans la colonne. Aucun touch-action ici — le geste de
728
+ glissement doit rester saisissable au travers du bouton (ADR-0005). -->
729
+ <button
730
+ type="button"
731
+ class="kt-dialog-container__sheet-close"
732
+ [attr.aria-label]="ktConfig.sheetCloseLabel"
733
+ (click)="onSheetCloseClick()"
734
+ >
735
+ <span class="kt-dialog-container__sheet-close-icon" aria-hidden="true"></span>
736
+ </button>
737
+ }
601
738
  @if (isSheet() && showSheetHandle()) {
602
- <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out :
603
- panelClass additionnel 'kt-dialog--no-handle' à l'ouverture). -->
739
+ <!-- Poignée DÉCORATIVE auto-rendue : la sheet s'attrape partout (opt-out : 'sheetHandle'). -->
604
740
  <div class="kt-dialog-container__sheet-handle" aria-hidden="true"></div>
605
741
  }
606
742
  <ng-template cdkPortalOutlet></ng-template>
@@ -609,17 +745,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
609
745
  }]
610
746
  }], ctorParameters: () => [] });
611
747
 
748
+ /** Retire les clés à `undefined` : une option NON fournie à l'ouverture ne doit pas écraser
749
+ la valeur du token global (une clé absente et une clé à `undefined` sont équivalentes ici). */
750
+ function definedOnly(source) {
751
+ return Object.fromEntries(Object.entries(source).filter(([, value]) => value !== undefined));
752
+ }
612
753
  function injectKtDialogOpener(component, baseConfig) {
613
754
  const dialog = inject(Dialog);
614
755
  const viewport = inject(KtViewport);
615
756
  // Optionnel : hors d'un arbre de vue (ex. service root), il n'y a pas de ViewContainerRef.
616
757
  const viewContainerRef = inject(ViewContainerRef, { optional: true }) ?? undefined;
617
758
  const idGen = inject(KtIdGenerator);
759
+ // Options maison au niveau application (partielles) : socle de la cascade résolue ci-dessous.
760
+ const appKtConfig = inject(KT_DIALOG_CONFIG, { optional: true });
618
761
  return (data, config) => {
619
762
  // La présentation (choisie par le dev) est résolue ICI, à l'ouverture : les variantes
620
763
  // responsive lisent le signal d'écran compact. Le panelClass final = présentation concrète + extras.
621
- const { presentation, panelClass, ...rest } = { ...baseConfig, ...config };
764
+ const { presentation, panelClass, sheetCloseButton, sheetCloseLabel, sheetHandle, ...rest } = {
765
+ ...baseConfig,
766
+ ...config,
767
+ };
622
768
  const extra = panelClass ? (Array.isArray(panelClass) ? panelClass : [panelClass]) : [];
769
+ // Cascade des options maison, résolue UNE SEULE FOIS ici (ADR-0003) : défaut → token
770
+ // application → option d'ouverture. Le conteneur ne relit JAMAIS le token de son côté ; il
771
+ // reçoit cette valeur déjà résolue par le canal `container.providers` du CDK (les
772
+ // `providers` de la config générale, eux, n'atteignent QUE le portail de contenu).
773
+ const ktConfig = {
774
+ ...DEFAULT_KT_DIALOG_CONFIG,
775
+ ...definedOnly(appKtConfig ?? {}),
776
+ ...definedOnly({ sheetCloseButton, sheetCloseLabel, sheetHandle }),
777
+ };
623
778
  // Dédoublonné : une classe déjà fournie par la présentation (ex. `kt-dialog`) et repassée
624
779
  // en `panelClass` par le consommateur ne doit pas apparaître deux fois.
625
780
  const merged = [...new Set([...resolveKtDialogPanelClass(presentation, viewport.isCompact()), ...extra])];
@@ -631,7 +786,11 @@ function injectKtDialogOpener(component, baseConfig) {
631
786
  // consommateur n'a pas câblé provideKtDialogDefaults(). `...rest` (config par appel) prime.
632
787
  ...KT_DIALOG_AAA_DEFAULTS,
633
788
  viewContainerRef,
634
- container: KtDialogContainer,
789
+ // Canal TYPÉ vers le conteneur : ces providers-là atterrissent bien dans SON injecteur.
790
+ container: {
791
+ type: KtDialogContainer,
792
+ providers: () => [{ provide: KT_DIALOG_CONFIG, useValue: ktConfig }],
793
+ },
635
794
  ariaLabelledBy: titleId,
636
795
  ariaDescribedBy: descId,
637
796
  ...rest,
@@ -953,5 +1112,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
953
1112
  * Generated bundle index. Do not edit.
954
1113
  */
955
1114
 
956
- export { KT_DIALOG_AAA_DEFAULTS, KtAlertDialog, KtConfirmDialog, KtDialogActions, KtDialogClose, KtDialogContainer, KtDialogContent, KtDialogDescription, KtDialogFocusInitial, KtDialogHeader, KtDialogImports, KtDialogSheetHandle, KtDialogTitle, KtQuickDialog, defineKtDialog, injectAlertDialog, injectConfirmDialog, injectKtDialogOpener, provideKtDialogDefaults, resolveKtDialogPanelClass };
1115
+ export { DEFAULT_KT_DIALOG_CONFIG, KT_DIALOG_AAA_DEFAULTS, KT_DIALOG_CONFIG, KtAlertDialog, KtConfirmDialog, KtDialogActions, KtDialogClose, KtDialogContainer, KtDialogContent, KtDialogDescription, KtDialogFocusInitial, KtDialogHeader, KtDialogImports, KtDialogSheetHandle, KtDialogTitle, KtQuickDialog, defineKtDialog, injectAlertDialog, injectConfirmDialog, injectKtDialogOpener, provideKtDialog, provideKtDialogDefaults, resolveKtDialogPanelClass };
957
1116
  //# sourceMappingURL=ktortu-aaa-dialog.mjs.map