@starterlib/ui 0.1.1 → 0.3.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 +87 -2
- package/fesm2022/starterlib-ui-interactions.mjs +296 -0
- package/fesm2022/starterlib-ui-interactions.mjs.map +1 -0
- package/fesm2022/starterlib-ui-surfaces.mjs +1339 -0
- package/fesm2022/starterlib-ui-surfaces.mjs.map +1 -0
- package/fesm2022/starterlib-ui.mjs +1570 -226
- package/fesm2022/starterlib-ui.mjs.map +1 -1
- package/package.json +21 -6
- package/types/starterlib-ui-interactions.d.ts +162 -0
- package/types/starterlib-ui-surfaces.d.ts +699 -0
- package/types/starterlib-ui.d.ts +1018 -123
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
2
|
+
import { DomPortal, CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, ElementRef, input, signal, DestroyRef, ChangeDetectionStrategy, Component, Directive, InjectionToken, afterNextRender, computed, Injectable, effect, output } from '@angular/core';
|
|
5
|
+
import * as i1 from '@angular/aria/menu';
|
|
6
|
+
import { Menu, MenuItem } from '@angular/aria/menu';
|
|
7
|
+
import { StIcon, ST_LIBELLES, StButton, StIconButton, ST_LOCALE, AnnonceLocale, formaterDuree } from '@starterlib/ui';
|
|
8
|
+
import { CdkDialogContainer, DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
9
|
+
export { DIALOG_DATA as DONNEES_DE_DIALOGUE } from '@angular/cdk/dialog';
|
|
10
|
+
import { hasModifierKey } from '@angular/cdk/keycodes';
|
|
11
|
+
import { Router, NavigationStart } from '@angular/router';
|
|
12
|
+
import { AriaDescriber } from '@angular/cdk/a11y';
|
|
13
|
+
import { DOCUMENT } from '@angular/common';
|
|
14
|
+
|
|
15
|
+
const POSITIONS$2 = {
|
|
16
|
+
debut: [
|
|
17
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
18
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
19
|
+
],
|
|
20
|
+
fin: [
|
|
21
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
|
|
22
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Menu d'actions. Le declencheur le designe, comme chez Material :
|
|
27
|
+
*
|
|
28
|
+
* ```html
|
|
29
|
+
* <button stMenuTrigger [menu]="compte" stButton>Actions</button>
|
|
30
|
+
*
|
|
31
|
+
* <st-menu #compte etiquette="Compte" (selection)="agir($event)">
|
|
32
|
+
* <button stMenuItem valeur="profil">Mon profil</button>
|
|
33
|
+
* <button stMenuItem valeur="quitter" ton="destructeur">Se deconnecter</button>
|
|
34
|
+
* </st-menu>
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ Pourquoi le declencheur est DEHORS et les elements DEDANS. La primitive
|
|
38
|
+
* `Menu` d'`@angular/aria` relie ses elements au menu par injection, et un
|
|
39
|
+
* contenu projete garde l'injecteur de l'endroit ou il est ECRIT — jamais celui
|
|
40
|
+
* qui le rend. Les elements doivent donc etre ecrits a l'interieur de l'element
|
|
41
|
+
* qui porte le menu, sans quoi ils ne s'y enregistrent pas : le clavier ne
|
|
42
|
+
* bouge plus, et rien ne le signale. Le declencheur, lui, n'a rien a faire dans
|
|
43
|
+
* un `role="menu"`, qui ne contient que des elements de menu.
|
|
44
|
+
*
|
|
45
|
+
* Le clavier vient d'Aria, le placement du CDK. Le socle n'ecrit ni l'un ni
|
|
46
|
+
* l'autre : deux motifs longs, que l'on croit finis avant de les avoir
|
|
47
|
+
* eprouves au lecteur d'ecran ou au bord de l'ecran.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ A l'ouverture, l'element est DEPLACE dans la couche d'overlay
|
|
50
|
+
* (`DomPortal`), il n'est pas recree : ses liaisons, son etat et son injecteur
|
|
51
|
+
* survivent au passage, ce qu'un gabarit reconstruit perdrait.
|
|
52
|
+
*/
|
|
53
|
+
class StMenu {
|
|
54
|
+
element = inject(ElementRef);
|
|
55
|
+
overlay = inject(Overlay);
|
|
56
|
+
aria = inject(Menu);
|
|
57
|
+
/** Nom du menu pour les technologies d'assistance. */
|
|
58
|
+
etiquette = input.required(/* @ts-ignore */
|
|
59
|
+
...(ngDevMode ? [{ debugName: "etiquette" }] : /* istanbul ignore next */ []));
|
|
60
|
+
alignement = input('debut', /* @ts-ignore */
|
|
61
|
+
...(ngDevMode ? [{ debugName: "alignement" }] : /* istanbul ignore next */ []));
|
|
62
|
+
ouvertInterne = signal(false, /* @ts-ignore */
|
|
63
|
+
...(ngDevMode ? [{ debugName: "ouvertInterne" }] : /* istanbul ignore next */ []));
|
|
64
|
+
ouvert = this.ouvertInterne.asReadonly();
|
|
65
|
+
couche = null;
|
|
66
|
+
declencheur = null;
|
|
67
|
+
constructor() {
|
|
68
|
+
// La primitive detecte la selection ; elle ne connait rien de la couche
|
|
69
|
+
// d'overlay, donc c'est ici que le menu se referme.
|
|
70
|
+
this.aria.itemSelected.subscribe(() => {
|
|
71
|
+
this.fermer();
|
|
72
|
+
});
|
|
73
|
+
inject(DestroyRef).onDestroy(() => {
|
|
74
|
+
this.detruireLaCouche();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
ouvrirDepuis(declencheur) {
|
|
78
|
+
if (this.ouvertInterne())
|
|
79
|
+
return;
|
|
80
|
+
this.declencheur = declencheur;
|
|
81
|
+
this.couche = this.overlay.create({
|
|
82
|
+
positionStrategy: this.overlay
|
|
83
|
+
.position()
|
|
84
|
+
.flexibleConnectedTo(declencheur)
|
|
85
|
+
.withPositions(POSITIONS$2[this.alignement()])
|
|
86
|
+
.withPush(true),
|
|
87
|
+
// Le menu suit son declencheur au defilement : le figer le laisserait
|
|
88
|
+
// seul au milieu de l'ecran, sans lien visible avec ce qu'il commande.
|
|
89
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
90
|
+
});
|
|
91
|
+
this.couche.attach(new DomPortal(this.element));
|
|
92
|
+
this.ouvertInterne.set(true);
|
|
93
|
+
this.couche.outsidePointerEvents().subscribe(() => {
|
|
94
|
+
this.fermer({ rendreLeFocus: false });
|
|
95
|
+
});
|
|
96
|
+
// Le panneau prend le focus : la primitive y tient l'indice de tabulation
|
|
97
|
+
// glissant, donc c'est elle qui decide de l'element actif.
|
|
98
|
+
this.element.nativeElement.focus();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ferme, et rend le focus au declencheur : sans cela, le focus retombe sur le
|
|
102
|
+
* corps du document, et la navigation clavier reprend en haut de page.
|
|
103
|
+
*/
|
|
104
|
+
fermer(options = {}) {
|
|
105
|
+
if (!this.ouvertInterne())
|
|
106
|
+
return;
|
|
107
|
+
this.ouvertInterne.set(false);
|
|
108
|
+
this.detruireLaCouche();
|
|
109
|
+
const declencheur = this.declencheur;
|
|
110
|
+
this.declencheur = null;
|
|
111
|
+
if (options.rendreLeFocus !== false)
|
|
112
|
+
declencheur?.focus();
|
|
113
|
+
}
|
|
114
|
+
basculerDepuis(declencheur) {
|
|
115
|
+
if (this.ouvertInterne())
|
|
116
|
+
this.fermer();
|
|
117
|
+
else
|
|
118
|
+
this.ouvrirDepuis(declencheur);
|
|
119
|
+
}
|
|
120
|
+
auClavier(evenement) {
|
|
121
|
+
if (evenement.key !== 'Escape')
|
|
122
|
+
return;
|
|
123
|
+
evenement.preventDefault();
|
|
124
|
+
this.fermer();
|
|
125
|
+
}
|
|
126
|
+
detruireLaCouche() {
|
|
127
|
+
// `detach` avant `dispose` : le portail rend l'element a sa place d'origine,
|
|
128
|
+
// et le composant reste utilisable pour une reouverture.
|
|
129
|
+
this.couche?.detach();
|
|
130
|
+
this.couche?.dispose();
|
|
131
|
+
this.couche = null;
|
|
132
|
+
}
|
|
133
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
134
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StMenu, isStandalone: true, selector: "st-menu", inputs: { etiquette: { classPropertyName: "etiquette", publicName: "etiquette", isSignal: true, isRequired: true, transformFunction: null }, alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "auClavier($event)" }, properties: { "attr.aria-label": "etiquette()", "attr.data-ouvert": "ouvert() ? \"\" : null" }, classAttribute: "st-menu" }, hostDirectives: [{ directive: i1.Menu, inputs: ["wrap", "wrap", "typeaheadDelay", "typeaheadDelay", "disabled", "disabled"], outputs: ["itemSelected", "selection"] }], ngImport: i0, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
135
|
+
}
|
|
136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, decorators: [{
|
|
137
|
+
type: Component,
|
|
138
|
+
args: [{ selector: 'st-menu', changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [
|
|
139
|
+
{
|
|
140
|
+
directive: Menu,
|
|
141
|
+
inputs: ['wrap', 'typeaheadDelay', 'disabled'],
|
|
142
|
+
outputs: ['itemSelected: selection'],
|
|
143
|
+
},
|
|
144
|
+
], host: {
|
|
145
|
+
class: 'st-menu',
|
|
146
|
+
'[attr.aria-label]': 'etiquette()',
|
|
147
|
+
'[attr.data-ouvert]': 'ouvert() ? "" : null',
|
|
148
|
+
'(keydown)': 'auClavier($event)',
|
|
149
|
+
}, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:none}:host([data-ouvert]){display:flex;flex-direction:column;gap:2px;min-inline-size:12rem;max-block-size:80vh;padding:var(--st-espacement-1);overflow-y:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-2)}\n"] }]
|
|
150
|
+
}], ctorParameters: () => [], propDecorators: { etiquette: [{ type: i0.Input, args: [{ isSignal: true, alias: "etiquette", required: true }] }], alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Element de menu, pose en ATTRIBUT sur un `<button>` natif — meme raison que
|
|
154
|
+
* `st-button` : une enveloppe casserait le comportement natif du bouton.
|
|
155
|
+
*
|
|
156
|
+
* Le clavier (fleches, Debut, Fin, saisie au vol, indice de tabulation
|
|
157
|
+
* glissant) vient de la primitive `MenuItem` d'`@angular/aria`, composee en
|
|
158
|
+
* directive d'hote. Le socle n'en reecrit aucune part : ces motifs sont longs a
|
|
159
|
+
* ecrire, faciles a croire finis, et c'est la ou l'accessibilite se perd.
|
|
160
|
+
*
|
|
161
|
+
* ⚠️ `valeur` est OBLIGATOIRE parce que la primitive l'exige, et c'est un bon
|
|
162
|
+
* defaut : c'est elle que `st-menu` emet a la selection. Un menu dont les
|
|
163
|
+
* elements n'ont pas d'identite stable oblige l'appelant a distinguer ses
|
|
164
|
+
* actions par leur libelle, donc par leur traduction.
|
|
165
|
+
*/
|
|
166
|
+
class StMenuItem {
|
|
167
|
+
icone = input(null, /* @ts-ignore */
|
|
168
|
+
...(ngDevMode ? [{ debugName: "icone" }] : /* istanbul ignore next */ []));
|
|
169
|
+
ton = input('neutre', /* @ts-ignore */
|
|
170
|
+
...(ngDevMode ? [{ debugName: "ton" }] : /* istanbul ignore next */ []));
|
|
171
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
172
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StMenuItem, isStandalone: true, selector: "button[stMenuItem]", inputs: { icone: { classPropertyName: "icone", publicName: "icone", isSignal: true, isRequired: false, transformFunction: null }, ton: { classPropertyName: "ton", publicName: "ton", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, properties: { "attr.data-ton": "ton()" }, classAttribute: "st-menu-item" }, hostDirectives: [{ directive: i1.MenuItem, inputs: ["value", "valeur", "disabled", "desactive", "role", "role"] }], ngImport: i0, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><ng-content /></span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;gap:var(--st-espacement-2);align-items:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"], dependencies: [{ kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
173
|
+
}
|
|
174
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, decorators: [{
|
|
175
|
+
type: Component,
|
|
176
|
+
args: [{ selector: 'button[stMenuItem]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StIcon], hostDirectives: [
|
|
177
|
+
{ directive: MenuItem, inputs: ['value: valeur', 'disabled: desactive', 'role'] },
|
|
178
|
+
], host: {
|
|
179
|
+
class: 'st-menu-item',
|
|
180
|
+
type: 'button',
|
|
181
|
+
'[attr.data-ton]': 'ton()',
|
|
182
|
+
}, template: "@if (icone(); as nom) {\n <st-icon [nom]=\"nom\" taille=\"sm\" />\n}\n\n<span class=\"st-menu-item__libelle\"><ng-content /></span>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;gap:var(--st-espacement-2);align-items:center;inline-size:100%;min-block-size:2.25rem;padding-block:var(--st-espacement-2);padding-inline:var(--st-espacement-3);font-family:var(--st-police-base);font-size:var(--st-taille-sm);color:var(--st-couleur-texte);text-align:start;cursor:pointer;background:transparent;border:0;border-radius:var(--st-rayon-sm)}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host:hover:not([aria-disabled=true]){background:var(--st-couleur-surface-survol)}:host[data-active=true]{background:var(--st-couleur-surface-survol)}:host[aria-disabled=true]{color:var(--st-couleur-texte-desactive);cursor:not-allowed}@media(any-pointer:coarse){:host{min-block-size:var(--st-cible-tactile-min)}}:host([data-ton=danger]){color:var(--st-couleur-danger)}:host([data-ton=danger]):hover:not([aria-disabled=true]),:host([data-ton=danger])[data-active=true]{background:var(--st-couleur-danger-discret)}.st-menu-item__libelle{flex:1}\n"] }]
|
|
183
|
+
}], propDecorators: { icone: [{ type: i0.Input, args: [{ isSignal: true, alias: "icone", required: false }] }], ton: [{ type: i0.Input, args: [{ isSignal: true, alias: "ton", required: false }] }] } });
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Controle qui ouvre un menu. Il le DESIGNE, il ne le contient pas :
|
|
187
|
+
*
|
|
188
|
+
* ```html
|
|
189
|
+
* <button stMenuTrigger [menu]="compte" stButton>Actions</button>
|
|
190
|
+
* <st-menu #compte etiquette="Compte">…</st-menu>
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ Le type de `menu` est importe en TYPE seulement : `st-menu` n'a pas besoin
|
|
194
|
+
* de cette directive, et l'importer dans les deux sens formerait un cycle.
|
|
195
|
+
*/
|
|
196
|
+
class StMenuTrigger {
|
|
197
|
+
element = inject(ElementRef).nativeElement;
|
|
198
|
+
menu = input.required(/* @ts-ignore */
|
|
199
|
+
...(ngDevMode ? [{ debugName: "menu" }] : /* istanbul ignore next */ []));
|
|
200
|
+
basculer() {
|
|
201
|
+
this.menu().basculerDepuis(this.element);
|
|
202
|
+
}
|
|
203
|
+
auClavier(evenement) {
|
|
204
|
+
// La fleche bas ouvre sans activer : convention des menus, et elle evite
|
|
205
|
+
// d'ouvrir a chaque frappe sur un controle qui porte deja une action.
|
|
206
|
+
if (evenement.key !== 'ArrowDown' || this.menu().ouvert())
|
|
207
|
+
return;
|
|
208
|
+
evenement.preventDefault();
|
|
209
|
+
this.menu().ouvrirDepuis(this.element);
|
|
210
|
+
}
|
|
211
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
212
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StMenuTrigger, isStandalone: true, selector: "[stMenuTrigger]", inputs: { menu: { classPropertyName: "menu", publicName: "menu", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "menu" }, listeners: { "click": "basculer()", "keydown": "auClavier($event)" }, properties: { "attr.aria-expanded": "menu().ouvert()", "attr.data-ouvert": "menu().ouvert() ? \"\" : null" } }, ngImport: i0 });
|
|
213
|
+
}
|
|
214
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuTrigger, decorators: [{
|
|
215
|
+
type: Directive,
|
|
216
|
+
args: [{
|
|
217
|
+
selector: '[stMenuTrigger]',
|
|
218
|
+
host: {
|
|
219
|
+
'aria-haspopup': 'menu',
|
|
220
|
+
'[attr.aria-expanded]': 'menu().ouvert()',
|
|
221
|
+
'[attr.data-ouvert]': 'menu().ouvert() ? "" : null',
|
|
222
|
+
'(click)': 'basculer()',
|
|
223
|
+
'(keydown)': 'auClavier($event)',
|
|
224
|
+
},
|
|
225
|
+
}]
|
|
226
|
+
}], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "menu", required: true }] }] } });
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Identifiant du titre du dialogue, decide a l'ouverture et fourni au contenu.
|
|
230
|
+
*
|
|
231
|
+
* ⚠️ Il est engendre par le service et non par le composant de titre : c'est la
|
|
232
|
+
* configuration du dialogue qui doit porter `aria-labelledby`, et elle est
|
|
233
|
+
* figee a l'ouverture. Un identifiant decide plus tard arriverait apres que le
|
|
234
|
+
* conteneur a lu sa configuration, et le dialogue s'ouvrirait sans nom.
|
|
235
|
+
*/
|
|
236
|
+
const IDENTIFIANT_DU_TITRE = new InjectionToken('st.dialogue.identifiant-du-titre');
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Le panneau du dialogue : sa surface, son arrondi, son ombre.
|
|
240
|
+
*
|
|
241
|
+
* ⚠️ Pourquoi ce composant existe. `CdkDialogContainer` n'a **aucune
|
|
242
|
+
* apparence** : c'est voulu de la part du CDK, qui ne decide d'aucun style. Le
|
|
243
|
+
* dialogue fonctionnait donc parfaitement — role, nom, piegeage du focus,
|
|
244
|
+
* Echap — tout en s'affichant SANS FOND, le texte flottant par-dessus la page.
|
|
245
|
+
* Aucun test ne pouvait le voir : jsdom ne calcule pas la mise en page, et les
|
|
246
|
+
* tests verifiaient le comportement, pas l'aspect.
|
|
247
|
+
*
|
|
248
|
+
* ⚠️ Un conteneur maison plutot qu'une feuille de style globale a importer :
|
|
249
|
+
* une SPA qui oublierait l'import retrouverait exactement le meme defaut, et
|
|
250
|
+
* l'oubli ne se verrait qu'a l'ecran. Ici, l'apparence voyage avec le
|
|
251
|
+
* composant.
|
|
252
|
+
*
|
|
253
|
+
* L'encapsulation reste EMULEE : seuls l'hote et le panneau qui l'enveloppe
|
|
254
|
+
* sont styles, par `:host` et `:host-context()`. Aucun contenu projete n'est
|
|
255
|
+
* vise, donc rien ne justifie de rendre ces styles globaux.
|
|
256
|
+
*/
|
|
257
|
+
class StDialogContainer extends CdkDialogContainer {
|
|
258
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContainer, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
259
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StDialogContainer, isStandalone: true, selector: "st-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : _config.ariaLabelledBy", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "_config.ariaDescribedBy || null" }, classAttribute: "st-dialog-container" }, usesInheritance: true, ngImport: i0, template: "<ng-template cdkPortalOutlet />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;flex-direction:column;max-block-size:inherit;overflow:hidden;color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border-radius:var(--st-rayon-lg);box-shadow:var(--st-ombre-xl)}:host(:focus){outline:none}:host-context(.st-tiroir){block-size:100%;max-block-size:100%;border-radius:0}:host-context(.st-tiroir--bas){border-start-start-radius:var(--st-rayon-lg);border-start-end-radius:var(--st-rayon-lg)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
260
|
+
}
|
|
261
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContainer, decorators: [{
|
|
262
|
+
type: Component,
|
|
263
|
+
args: [{ selector: 'st-dialog-container', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkPortalOutlet], host: {
|
|
264
|
+
class: 'st-dialog-container',
|
|
265
|
+
'[attr.role]': '_config.role',
|
|
266
|
+
'[attr.aria-modal]': '_config.ariaModal',
|
|
267
|
+
'[attr.aria-labelledby]': '_config.ariaLabel ? null : _config.ariaLabelledBy',
|
|
268
|
+
'[attr.aria-label]': '_config.ariaLabel',
|
|
269
|
+
'[attr.aria-describedby]': '_config.ariaDescribedBy || null',
|
|
270
|
+
tabindex: '-1',
|
|
271
|
+
}, template: "<ng-template cdkPortalOutlet />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;flex-direction:column;max-block-size:inherit;overflow:hidden;color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border-radius:var(--st-rayon-lg);box-shadow:var(--st-ombre-xl)}:host(:focus){outline:none}:host-context(.st-tiroir){block-size:100%;max-block-size:100%;border-radius:0}:host-context(.st-tiroir--bas){border-start-start-radius:var(--st-rayon-lg);border-start-end-radius:var(--st-rayon-lg)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"] }]
|
|
272
|
+
}] });
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Titre du dialogue. Il porte l'identifiant que la configuration a designe en
|
|
276
|
+
* `aria-labelledby` : c'est lui qui NOMME le dialogue.
|
|
277
|
+
*
|
|
278
|
+
* ⚠️ Un `h2` et non un `h1` : le dialogue s'ouvre par-dessus une page qui a
|
|
279
|
+
* deja son titre de niveau 1, et deux `h1` cassent la table des matieres que
|
|
280
|
+
* les lecteurs d'ecran construisent.
|
|
281
|
+
*/
|
|
282
|
+
class StDialogTitle {
|
|
283
|
+
identifiant = inject(IDENTIFIANT_DU_TITRE, { optional: true });
|
|
284
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogTitle, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
285
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StDialogTitle, isStandalone: true, selector: "st-dialog-title", host: { classAttribute: "st-dialog-title" }, ngImport: i0, template: "<h2 class=\"st-dialog-title__texte\" [attr.id]=\"identifiant\"><ng-content /></h2>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;padding:var(--st-espacement-5) var(--st-espacement-5) 0}.st-dialog-title__texte{margin:0;font-family:var(--st-police-base);font-size:var(--st-taille-xl);font-weight:var(--st-graisse-semi);line-height:var(--st-interligne-serre);color:var(--st-couleur-texte);text-wrap:balance}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
286
|
+
}
|
|
287
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogTitle, decorators: [{
|
|
288
|
+
type: Component,
|
|
289
|
+
args: [{ selector: 'st-dialog-title', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-dialog-title' }, template: "<h2 class=\"st-dialog-title__texte\" [attr.id]=\"identifiant\"><ng-content /></h2>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;padding:var(--st-espacement-5) var(--st-espacement-5) 0}.st-dialog-title__texte{margin:0;font-family:var(--st-police-base);font-size:var(--st-taille-xl);font-weight:var(--st-graisse-semi);line-height:var(--st-interligne-serre);color:var(--st-couleur-texte);text-wrap:balance}\n"] }]
|
|
290
|
+
}] });
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Corps du dialogue, defilant quand il deborde.
|
|
294
|
+
*
|
|
295
|
+
* ⚠️ Le defilement est ICI et non sur le dialogue entier : titre et actions
|
|
296
|
+
* doivent rester visibles. Un dialogue long dont les boutons partent hors de
|
|
297
|
+
* l'ecran est le defaut classique, et il ne se voit qu'a 200 % d'echelle ou sur
|
|
298
|
+
* un ecran bas.
|
|
299
|
+
*
|
|
300
|
+
* ⚠️ **Focalisable seulement quand il defile.** Une zone defilante sans
|
|
301
|
+
* controle doit recevoir le focus, sinon le clavier ne peut pas la faire
|
|
302
|
+
* defiler — le texte d'une condition generale, par exemple. Mais la premiere
|
|
303
|
+
* version posait `tabindex="0"` sans condition : sur un dialogue court, la zone
|
|
304
|
+
* de texte devenait le premier arret de tabulation, AVANT le bouton
|
|
305
|
+
* d'annulation, contrairement a ce que `StConfirmDialog` affirmait.
|
|
306
|
+
*/
|
|
307
|
+
class StDialogContent {
|
|
308
|
+
/** `aucun` pour un contenu qui gere ses propres marges, comme un tableau. */
|
|
309
|
+
rembourrage = input('md', /* @ts-ignore */
|
|
310
|
+
...(ngDevMode ? [{ debugName: "rembourrage" }] : /* istanbul ignore next */ []));
|
|
311
|
+
defilante = signal(false, /* @ts-ignore */
|
|
312
|
+
...(ngDevMode ? [{ debugName: "defilante" }] : /* istanbul ignore next */ []));
|
|
313
|
+
constructor() {
|
|
314
|
+
const element = inject(ElementRef).nativeElement;
|
|
315
|
+
const destruction = inject(DestroyRef);
|
|
316
|
+
// Mesure apres le rendu, puis a chaque changement de taille ou de contenu :
|
|
317
|
+
// des donnees arrivees apres l'ouverture peuvent faire deborder un dialogue
|
|
318
|
+
// qui ne debordait pas.
|
|
319
|
+
afterNextRender(() => {
|
|
320
|
+
const mesurer = () => {
|
|
321
|
+
this.defilante.set(element.scrollHeight > element.clientHeight + 1);
|
|
322
|
+
};
|
|
323
|
+
mesurer();
|
|
324
|
+
const observateurs = [];
|
|
325
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
326
|
+
const taille = new ResizeObserver(mesurer);
|
|
327
|
+
taille.observe(element);
|
|
328
|
+
observateurs.push(taille);
|
|
329
|
+
}
|
|
330
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
331
|
+
const contenu = new MutationObserver(mesurer);
|
|
332
|
+
contenu.observe(element, { childList: true, subtree: true, characterData: true });
|
|
333
|
+
observateurs.push(contenu);
|
|
334
|
+
}
|
|
335
|
+
destruction.onDestroy(() => {
|
|
336
|
+
for (const observateur of observateurs)
|
|
337
|
+
observateur.disconnect();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
342
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StDialogContent, isStandalone: true, selector: "st-dialog-content", inputs: { rembourrage: { classPropertyName: "rembourrage", publicName: "rembourrage", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.tabindex": "defilante() ? 0 : null", "attr.data-rembourrage": "rembourrage()" }, classAttribute: "st-dialog-content" }, ngImport: i0, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host{display:block;overflow-y:auto;max-block-size:60vh;color:var(--st-couleur-texte)}:host([data-rembourrage=md]){padding:var(--st-espacement-4) var(--st-espacement-5)}:host([data-rembourrage=aucun]){padding:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
343
|
+
}
|
|
344
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogContent, decorators: [{
|
|
345
|
+
type: Component,
|
|
346
|
+
args: [{ selector: 'st-dialog-content', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
347
|
+
class: 'st-dialog-content',
|
|
348
|
+
'[attr.tabindex]': 'defilante() ? 0 : null',
|
|
349
|
+
'[attr.data-rembourrage]': 'rembourrage()',
|
|
350
|
+
}, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:-2px}:host{display:block;overflow-y:auto;max-block-size:60vh;color:var(--st-couleur-texte)}:host([data-rembourrage=md]){padding:var(--st-espacement-4) var(--st-espacement-5)}:host([data-rembourrage=aucun]){padding:0}\n"] }]
|
|
351
|
+
}], ctorParameters: () => [], propDecorators: { rembourrage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rembourrage", required: false }] }] } });
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Barre d'actions du dialogue.
|
|
355
|
+
*
|
|
356
|
+
* ⚠️ L'action de confirmation est a DROITE et l'annulation a gauche, dans cet
|
|
357
|
+
* ordre dans le DOM : l'ordre de tabulation doit suivre l'ordre visuel, sinon
|
|
358
|
+
* le clavier et la souris ne donnent pas le meme parcours. Inverser
|
|
359
|
+
* visuellement par `row-reverse` casserait cette correspondance.
|
|
360
|
+
*/
|
|
361
|
+
class StDialogActions {
|
|
362
|
+
alignement = input('fin', /* @ts-ignore */
|
|
363
|
+
...(ngDevMode ? [{ debugName: "alignement" }] : /* istanbul ignore next */ []));
|
|
364
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogActions, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
365
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StDialogActions, isStandalone: true, selector: "st-dialog-actions", inputs: { alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-alignement": "alignement()" }, classAttribute: "st-dialog-actions" }, ngImport: i0, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;flex-wrap:wrap;gap:var(--st-espacement-2);align-items:center;padding:var(--st-espacement-4) var(--st-espacement-5) var(--st-espacement-5)}:host([data-alignement=fin]){justify-content:flex-end}:host([data-alignement=debut]){justify-content:flex-start}:host([data-alignement=entre]){justify-content:space-between}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
366
|
+
}
|
|
367
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialogActions, decorators: [{
|
|
368
|
+
type: Component,
|
|
369
|
+
args: [{ selector: 'st-dialog-actions', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
370
|
+
class: 'st-dialog-actions',
|
|
371
|
+
'[attr.data-alignement]': 'alignement()',
|
|
372
|
+
}, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:flex;flex-wrap:wrap;gap:var(--st-espacement-2);align-items:center;padding:var(--st-espacement-4) var(--st-espacement-5) var(--st-espacement-5)}:host([data-alignement=fin]){justify-content:flex-end}:host([data-alignement=debut]){justify-content:flex-start}:host([data-alignement=entre]){justify-content:space-between}\n"] }]
|
|
373
|
+
}], propDecorators: { alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
|
|
374
|
+
|
|
375
|
+
let compteur$3 = 0;
|
|
376
|
+
/**
|
|
377
|
+
* Dialogue de confirmation.
|
|
378
|
+
*
|
|
379
|
+
* ⚠️ **Le bouton de confirmation n'est jamais celui qui recoit le focus.** Le
|
|
380
|
+
* dialogue entier le recoit (voir `StDialog`), et le premier arret de
|
|
381
|
+
* tabulation est l'annulation. Un dialogue destructeur dont le bouton dangereux
|
|
382
|
+
* est deja focalise se valide par une seule touche Entree — c'est-a-dire par le
|
|
383
|
+
* geste que la personne etait en train de faire quand il s'est ouvert.
|
|
384
|
+
*/
|
|
385
|
+
class StConfirmDialog {
|
|
386
|
+
reference = inject(DialogRef);
|
|
387
|
+
libelles = inject(ST_LIBELLES);
|
|
388
|
+
demande = inject(DIALOG_DATA);
|
|
389
|
+
saisie = signal('', /* @ts-ignore */
|
|
390
|
+
...(ngDevMode ? [{ debugName: "saisie" }] : /* istanbul ignore next */ []));
|
|
391
|
+
/** Unique par instance : deux confirmations empilees dupliqueraient un identifiant fixe. */
|
|
392
|
+
identifiantDeSaisie = `st-confirmation-saisie-${(compteur$3 += 1).toString()}`;
|
|
393
|
+
ton = computed(() => this.demande.destructeur === true ? 'danger' : 'primaire', /* @ts-ignore */
|
|
394
|
+
...(ngDevMode ? [{ debugName: "ton" }] : /* istanbul ignore next */ []));
|
|
395
|
+
libelleConfirmation = computed(() => this.demande.libelleConfirmation ?? this.libelles().confirmer, /* @ts-ignore */
|
|
396
|
+
...(ngDevMode ? [{ debugName: "libelleConfirmation" }] : /* istanbul ignore next */ []));
|
|
397
|
+
libelleAnnulation = computed(() => this.demande.libelleAnnulation ?? this.libelles().annuler, /* @ts-ignore */
|
|
398
|
+
...(ngDevMode ? [{ debugName: "libelleAnnulation" }] : /* istanbul ignore next */ []));
|
|
399
|
+
libelleRecopie = computed(() => this.libelles().recopierPourConfirmer(this.demande.saisieDeConfirmation ?? ''), /* @ts-ignore */
|
|
400
|
+
...(ngDevMode ? [{ debugName: "libelleRecopie" }] : /* istanbul ignore next */ []));
|
|
401
|
+
/**
|
|
402
|
+
* ⚠️ La comparaison ignore la casse et les espaces de bord, mais RIEN
|
|
403
|
+
* d'autre. Exiger la casse exacte transforme une confirmation en jeu
|
|
404
|
+
* d'adresse, et pousse a copier-coller — ce qui annule precisement l'effet
|
|
405
|
+
* recherche.
|
|
406
|
+
*/
|
|
407
|
+
confirmationPossible = computed(() => {
|
|
408
|
+
const attendu = this.demande.saisieDeConfirmation;
|
|
409
|
+
if (attendu === undefined)
|
|
410
|
+
return true;
|
|
411
|
+
return this.saisie().trim().toLocaleLowerCase() === attendu.trim().toLocaleLowerCase();
|
|
412
|
+
}, /* @ts-ignore */
|
|
413
|
+
...(ngDevMode ? [{ debugName: "confirmationPossible" }] : /* istanbul ignore next */ []));
|
|
414
|
+
auChangement(evenement) {
|
|
415
|
+
this.saisie.set(evenement.target.value);
|
|
416
|
+
}
|
|
417
|
+
confirmer() {
|
|
418
|
+
if (!this.confirmationPossible())
|
|
419
|
+
return;
|
|
420
|
+
this.reference.close(true);
|
|
421
|
+
}
|
|
422
|
+
annuler() {
|
|
423
|
+
this.reference.close(false);
|
|
424
|
+
}
|
|
425
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
426
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StConfirmDialog, isStandalone: true, selector: "st-confirm-dialog", host: { classAttribute: "st-confirm-dialog" }, ngImport: i0, template: "<st-dialog-title>{{ demande.titre }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-confirm-dialog__message\">{{ demande.message }}</p>\n\n @if (demande.saisieDeConfirmation !== undefined) {\n <div class=\"st-confirm-dialog__garde\">\n <label class=\"st-confirm-dialog__etiquette\" [attr.for]=\"identifiantDeSaisie\">\n {{ libelleRecopie() }}\n </label>\n <input\n [id]=\"identifiantDeSaisie\"\n class=\"st-confirm-dialog__champ\"\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"saisie()\"\n (input)=\"auChangement($event)\"\n />\n </div>\n }\n</st-dialog-content>\n\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"annuler()\">\n {{ libelleAnnulation() }}\n </button>\n <button\n stButton\n type=\"button\"\n [variante]=\"ton() === 'danger' ? 'danger' : 'primaire'\"\n [indisponible]=\"!confirmationPossible()\"\n (click)=\"confirmer()\"\n >\n {{ libelleConfirmation() }}\n </button>\n</st-dialog-actions>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-confirm-dialog__message{margin:0}.st-confirm-dialog__garde{display:flex;flex-direction:column;gap:var(--st-espacement-2);margin-block-start:var(--st-espacement-4)}.st-confirm-dialog__etiquette{font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-confirm-dialog__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-confirm-dialog__champ{padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-sm)}\n"], dependencies: [{ kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }, { kind: "component", type: StDialogTitle, selector: "st-dialog-title" }, { kind: "component", type: StDialogContent, selector: "st-dialog-content", inputs: ["rembourrage"] }, { kind: "component", type: StDialogActions, selector: "st-dialog-actions", inputs: ["alignement"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
427
|
+
}
|
|
428
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StConfirmDialog, decorators: [{
|
|
429
|
+
type: Component,
|
|
430
|
+
args: [{ selector: 'st-confirm-dialog', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StButton, StDialogTitle, StDialogContent, StDialogActions], host: { class: 'st-confirm-dialog' }, template: "<st-dialog-title>{{ demande.titre }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-confirm-dialog__message\">{{ demande.message }}</p>\n\n @if (demande.saisieDeConfirmation !== undefined) {\n <div class=\"st-confirm-dialog__garde\">\n <label class=\"st-confirm-dialog__etiquette\" [attr.for]=\"identifiantDeSaisie\">\n {{ libelleRecopie() }}\n </label>\n <input\n [id]=\"identifiantDeSaisie\"\n class=\"st-confirm-dialog__champ\"\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"saisie()\"\n (input)=\"auChangement($event)\"\n />\n </div>\n }\n</st-dialog-content>\n\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"annuler()\">\n {{ libelleAnnulation() }}\n </button>\n <button\n stButton\n type=\"button\"\n [variante]=\"ton() === 'danger' ? 'danger' : 'primaire'\"\n [indisponible]=\"!confirmationPossible()\"\n (click)=\"confirmer()\"\n >\n {{ libelleConfirmation() }}\n </button>\n</st-dialog-actions>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-confirm-dialog__message{margin:0}.st-confirm-dialog__garde{display:flex;flex-direction:column;gap:var(--st-espacement-2);margin-block-start:var(--st-espacement-4)}.st-confirm-dialog__etiquette{font-size:var(--st-taille-sm);font-weight:var(--st-graisse-semi);color:var(--st-couleur-texte)}.st-confirm-dialog__champ:focus-visible{outline:2px solid var(--st-couleur-anneau-focus);outline-offset:2px}.st-confirm-dialog__champ{padding:var(--st-espacement-2) var(--st-espacement-3);font:inherit;color:var(--st-couleur-texte);background:var(--st-couleur-surface);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-sm)}\n"] }]
|
|
431
|
+
}] });
|
|
432
|
+
|
|
433
|
+
let compteur$2 = 0;
|
|
434
|
+
/**
|
|
435
|
+
* Ouvre des dialogues modaux.
|
|
436
|
+
*
|
|
437
|
+
* ⚠️ Monte sur `@angular/cdk/dialog`, et c'est un choix defendu : le piegeage
|
|
438
|
+
* du focus, sa restauration au declencheur, `aria-modal`, la mise en retrait du
|
|
439
|
+
* reste de la page, Echap et le blocage du defilement sont longs a ecrire,
|
|
440
|
+
* faciles a croire finis, et c'est exactement la que l'accessibilite se perd.
|
|
441
|
+
* Le socle n'en reecrit aucune part.
|
|
442
|
+
*
|
|
443
|
+
* Ce qu'il ajoute au CDK : le focus sur le dialogue, `aria-modal`, un fond inerte
|
|
444
|
+
* qui laisse Echap fermer, et la fermeture a toute navigation du routeur.
|
|
445
|
+
*/
|
|
446
|
+
class StDialog {
|
|
447
|
+
cdk = inject(Dialog);
|
|
448
|
+
overlay = inject(Overlay);
|
|
449
|
+
/** Une fonction de fermeture par dialogue ouvert : c'est tout ce qu'il faut garder. */
|
|
450
|
+
ouverts = new Set();
|
|
451
|
+
constructor() {
|
|
452
|
+
// ⚠️ `closeOnNavigation` du CDK ne couvre que l'HISTORIQUE du navigateur, sa
|
|
453
|
+
// documentation le dit : ni un lien du routeur, ni une navigation programmee.
|
|
454
|
+
// Un lien pose dans un tiroir de filtres changeait l'ecran dessous et
|
|
455
|
+
// laissait le tiroir pose dessus. Le routeur est optionnel : sans lui, il n'y
|
|
456
|
+
// a pas de navigation a suivre.
|
|
457
|
+
const routeur = inject(Router, { optional: true });
|
|
458
|
+
if (routeur === null)
|
|
459
|
+
return;
|
|
460
|
+
const abonnement = routeur.events.subscribe((evenement) => {
|
|
461
|
+
if (!(evenement instanceof NavigationStart))
|
|
462
|
+
return;
|
|
463
|
+
for (const fermer of [...this.ouverts])
|
|
464
|
+
fermer();
|
|
465
|
+
});
|
|
466
|
+
inject(DestroyRef).onDestroy(() => {
|
|
467
|
+
abonnement.unsubscribe();
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Ouvre un composant, ou un GABARIT.
|
|
472
|
+
*
|
|
473
|
+
* Un gabarit sert quand le contenu vit deja dans le composant appelant — un
|
|
474
|
+
* tiroir de navigation qui deplace ses propres liens sur petit ecran. ⚠️ Son
|
|
475
|
+
* injecteur reste celui de l'endroit ou il est ECRIT : un `st-dialog-title`
|
|
476
|
+
* pose dedans ne recoit pas l'identifiant de titre, et le dialogue se nomme
|
|
477
|
+
* alors par `etiquette`.
|
|
478
|
+
*/
|
|
479
|
+
ouvrir(composant, options = {}) {
|
|
480
|
+
compteur$2 += 1;
|
|
481
|
+
const identifiantDuTitre = `st-dialogue-titre-${compteur$2.toString()}`;
|
|
482
|
+
const fournisseurs = [
|
|
483
|
+
{ provide: IDENTIFIANT_DU_TITRE, useValue: identifiantDuTitre },
|
|
484
|
+
];
|
|
485
|
+
// L'argument de type porte le resultat jusqu'a l'appelant : l'omettre fait
|
|
486
|
+
// retomber l'inference sur `unknown`, et `fermer(resultat)` n'accepte plus
|
|
487
|
+
// rien de type.
|
|
488
|
+
const reference = this.cdk.open(composant, {
|
|
489
|
+
data: options.donnees,
|
|
490
|
+
providers: () => fournisseurs,
|
|
491
|
+
// ⚠️ Le conteneur du CDK n'a AUCUNE apparence : le dialogue s'affichait
|
|
492
|
+
// sans fond, le texte flottant sur la page. Voir `StDialogContainer`.
|
|
493
|
+
container: StDialogContainer,
|
|
494
|
+
...this.geometrie(options),
|
|
495
|
+
panelClass: this.classes(options),
|
|
496
|
+
disableClose: options.fermetureInterdite === true || options.fondInerte === true,
|
|
497
|
+
// ⚠️ Nomme par son TITRE quand il en a un. Le dialogue sans nom accessible
|
|
498
|
+
// s'annonce « dialogue » et rien d'autre : la personne sait qu'une couche
|
|
499
|
+
// s'est ouverte, sans savoir laquelle.
|
|
500
|
+
ariaLabelledBy: identifiantDuTitre,
|
|
501
|
+
ariaLabel: options.etiquette ?? null,
|
|
502
|
+
ariaDescribedBy: options.descriptionDe ?? null,
|
|
503
|
+
// ⚠️ Pose EN PLUS du `aria-hidden` que le CDK met sur le reste de la page.
|
|
504
|
+
// Les deux signaux ne sont pas redondants : certaines technologies
|
|
505
|
+
// d'assistance ne lisent que l'un, et d'autres que l'autre.
|
|
506
|
+
ariaModal: true,
|
|
507
|
+
// ⚠️ Le focus va au DIALOGUE, non au premier element focalisable. Le
|
|
508
|
+
// defaut du CDK (`first-tabbable`) pose souvent le focus sur la croix de
|
|
509
|
+
// fermeture : le lecteur d'ecran annonce « Fermer », et rien de ce que le
|
|
510
|
+
// dialogue dit. Sur le conteneur, il lit le nom puis le contenu.
|
|
511
|
+
autoFocus: options.focusInitial ?? 'dialog',
|
|
512
|
+
role: options.role ?? 'dialog',
|
|
513
|
+
// ⚠️ Le focus retourne au declencheur a la fermeture. Sans cela, il
|
|
514
|
+
// retombe sur le corps du document et la navigation repart du haut.
|
|
515
|
+
restoreFocus: true,
|
|
516
|
+
// Une navigation arriere doit fermer la couche : sinon le dialogue reste
|
|
517
|
+
// pose sur un ecran qui a change dessous.
|
|
518
|
+
closeOnNavigation: true,
|
|
519
|
+
});
|
|
520
|
+
if (options.fondInerte === true && options.fermetureInterdite !== true) {
|
|
521
|
+
reference.keydownEvents.subscribe((evenement) => {
|
|
522
|
+
// `key` et non `keyCode`, deprecie — c'est pourtant `keyCode` que teste le
|
|
523
|
+
// CDK pour sa propre gestion d'Echap, desactivee ici par `disableClose`.
|
|
524
|
+
if (evenement.key !== 'Escape' || hasModifierKey(evenement))
|
|
525
|
+
return;
|
|
526
|
+
evenement.preventDefault();
|
|
527
|
+
reference.close();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const fermerSansResultat = () => {
|
|
531
|
+
reference.close();
|
|
532
|
+
};
|
|
533
|
+
this.ouverts.add(fermerSansResultat);
|
|
534
|
+
return {
|
|
535
|
+
ferme: new Promise((resoudre) => {
|
|
536
|
+
reference.closed.subscribe((resultat) => {
|
|
537
|
+
this.ouverts.delete(fermerSansResultat);
|
|
538
|
+
resoudre(resultat);
|
|
539
|
+
});
|
|
540
|
+
}),
|
|
541
|
+
fermer: (resultat) => {
|
|
542
|
+
reference.close(resultat);
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* ⚠️ Un tiroir occupe toute la hauteur du bord ou il s'ancre, et une feuille
|
|
548
|
+
* inferieure toute la largeur. Sans cela, le panneau flotte au milieu et ne
|
|
549
|
+
* se lit plus comme un tiroir.
|
|
550
|
+
*/
|
|
551
|
+
geometrie(options) {
|
|
552
|
+
const cote = options.cote;
|
|
553
|
+
if (cote === undefined) {
|
|
554
|
+
return { width: options.largeur ?? 'min(32rem, calc(100vw - 2rem))' };
|
|
555
|
+
}
|
|
556
|
+
const position = this.overlay.position().global();
|
|
557
|
+
if (cote === 'bas') {
|
|
558
|
+
position.bottom('0').left('0');
|
|
559
|
+
return { width: '100%', positionStrategy: position };
|
|
560
|
+
}
|
|
561
|
+
// ⚠️ `start` et `end` du CDK, et non `left` et `right` : ils suivent le sens
|
|
562
|
+
// d'ecriture que le CDK connait deja. Une premiere version resolvait le
|
|
563
|
+
// sens a la main en lisant `dir` sur `<html>` — alors que le CDK le lit
|
|
564
|
+
// aussi sur `<body>`. Deux sources de verite pour la meme question finissent
|
|
565
|
+
// par diverger, et le tiroir s'ouvre alors du mauvais cote.
|
|
566
|
+
if (cote === 'fin')
|
|
567
|
+
position.end('0');
|
|
568
|
+
else
|
|
569
|
+
position.start('0');
|
|
570
|
+
position.top('0');
|
|
571
|
+
return {
|
|
572
|
+
width: options.largeur ?? 'min(28rem, calc(100vw - 3rem))',
|
|
573
|
+
height: '100%',
|
|
574
|
+
positionStrategy: position,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
classes(options) {
|
|
578
|
+
const classes = ['st-dialogue'];
|
|
579
|
+
if (options.cote !== undefined)
|
|
580
|
+
classes.push('st-tiroir', `st-tiroir--${options.cote}`);
|
|
581
|
+
if (options.classe !== undefined)
|
|
582
|
+
classes.push(options.classe);
|
|
583
|
+
return classes;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Demande une confirmation, et rend `true` seulement si elle est donnee.
|
|
587
|
+
*
|
|
588
|
+
* ⚠️ La fermeture par Echap ou par le fond rend `false` : quitter n'est pas
|
|
589
|
+
* confirmer. C'est pourquoi la valeur est comparee explicitement plutot que
|
|
590
|
+
* testee pour sa veracite — `undefined` doit valoir non.
|
|
591
|
+
*/
|
|
592
|
+
async confirmer(demande) {
|
|
593
|
+
const reference = this.ouvrir(StConfirmDialog, {
|
|
594
|
+
donnees: demande,
|
|
595
|
+
// ⚠️ Une action destructrice ne se ferme pas d'un clic a cote : le clic
|
|
596
|
+
// hors de la couche est le geste le plus facile a faire par megarde. Echap,
|
|
597
|
+
// lui, reste permis — il vaut annuler.
|
|
598
|
+
fondInerte: demande.destructeur === true,
|
|
599
|
+
});
|
|
600
|
+
return (await reference.ferme) === true;
|
|
601
|
+
}
|
|
602
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
603
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, providedIn: 'root' });
|
|
604
|
+
}
|
|
605
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StDialog, decorators: [{
|
|
606
|
+
type: Injectable,
|
|
607
|
+
args: [{ providedIn: 'root' }]
|
|
608
|
+
}], ctorParameters: () => [] });
|
|
609
|
+
|
|
610
|
+
/** Une FORME par ton, jamais la couleur seule. */
|
|
611
|
+
const ICONE = {
|
|
612
|
+
neutre: 'cercle-info',
|
|
613
|
+
primaire: 'cercle-info',
|
|
614
|
+
succes: 'cercle-coche',
|
|
615
|
+
avertissement: 'triangle-alerte',
|
|
616
|
+
danger: 'cercle-croix',
|
|
617
|
+
};
|
|
618
|
+
const DUREE_PAR_DEFAUT_MS = 5000;
|
|
619
|
+
/**
|
|
620
|
+
* ⚠️ Au-dela, les plus anciens messages SANS action sont retires. Une pile qui
|
|
621
|
+
* grandit sans limite finit par couvrir l'ecran, en poussant hors champ ce qui
|
|
622
|
+
* vient d'arriver.
|
|
623
|
+
*/
|
|
624
|
+
const MAXIMUM_AFFICHE = 3;
|
|
625
|
+
/**
|
|
626
|
+
* Meme raison que dans `StAnnonceur` du noyau : la region doit etre observee
|
|
627
|
+
* avant de recevoir son texte, et un texte identique repose dans la meme tache
|
|
628
|
+
* ne produit aucune mutation.
|
|
629
|
+
*/
|
|
630
|
+
const DELAI_D_ANNONCE_MS = 120;
|
|
631
|
+
/**
|
|
632
|
+
* Pile des messages ephemeres. Un seul exemplaire, pose par `StToast`.
|
|
633
|
+
*
|
|
634
|
+
* ⚠️ **Les messages visibles ne portent AUCUN role.** L'annonce passe par deux
|
|
635
|
+
* regions vivantes permanentes, creees vides avec le conteneur. La premiere
|
|
636
|
+
* version posait `role="status"` sur chaque message insere avec son texte :
|
|
637
|
+
* c'est la sequence que les lecteurs d'ecran n'annoncent pas de facon fiable,
|
|
638
|
+
* puisqu'ils rapportent les CHANGEMENTS d'une region deja observee. Les
|
|
639
|
+
* confirmations — le cas courant — risquaient le silence.
|
|
640
|
+
*
|
|
641
|
+
* ⚠️ Il ne prend JAMAIS le focus. Un message qui s'affiche pendant une saisie
|
|
642
|
+
* et vole le curseur fait perdre la frappe en cours.
|
|
643
|
+
*/
|
|
644
|
+
class StToastContainer {
|
|
645
|
+
libelles = inject(ST_LIBELLES);
|
|
646
|
+
pile = signal([], /* @ts-ignore */
|
|
647
|
+
...(ngDevMode ? [{ debugName: "pile" }] : /* istanbul ignore next */ []));
|
|
648
|
+
minuteries = new Map();
|
|
649
|
+
// Un minuteur PAR region : partage, une confirmation suivie d'une erreur dans
|
|
650
|
+
// la meme tache perdait la confirmation.
|
|
651
|
+
minuteriesDAnnonce = new Map();
|
|
652
|
+
compteur = 0;
|
|
653
|
+
messages = this.pile.asReadonly();
|
|
654
|
+
annoncePolie = signal('', /* @ts-ignore */
|
|
655
|
+
...(ngDevMode ? [{ debugName: "annoncePolie" }] : /* istanbul ignore next */ []));
|
|
656
|
+
annonceAssertive = signal('', /* @ts-ignore */
|
|
657
|
+
...(ngDevMode ? [{ debugName: "annonceAssertive" }] : /* istanbul ignore next */ []));
|
|
658
|
+
libelleFermeture = computed(() => this.libelles().fermerLeMessage, /* @ts-ignore */
|
|
659
|
+
...(ngDevMode ? [{ debugName: "libelleFermeture" }] : /* istanbul ignore next */ []));
|
|
660
|
+
constructor() {
|
|
661
|
+
// Une micro-SPA federee se demonte : ses minuteries ne doivent pas lui
|
|
662
|
+
// survivre et agir sur un composant detruit.
|
|
663
|
+
inject(DestroyRef).onDestroy(() => {
|
|
664
|
+
for (const minuterie of this.minuteries.values())
|
|
665
|
+
clearTimeout(minuterie);
|
|
666
|
+
this.minuteries.clear();
|
|
667
|
+
for (const minuterie of this.minuteriesDAnnonce.values())
|
|
668
|
+
clearTimeout(minuterie);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
ajouter(demande) {
|
|
672
|
+
this.compteur += 1;
|
|
673
|
+
const identifiant = this.compteur;
|
|
674
|
+
const toast = { ...demande, identifiant, ton: demande.ton ?? 'neutre' };
|
|
675
|
+
this.pile.update((existants) => this.borner([...existants, toast]));
|
|
676
|
+
this.programmer(toast);
|
|
677
|
+
this.annoncer(toast.message, toast.ton === 'danger' ? 'assertif' : 'poli');
|
|
678
|
+
return identifiant;
|
|
679
|
+
}
|
|
680
|
+
retirer(identifiant) {
|
|
681
|
+
this.arreterLaMinuterie(identifiant);
|
|
682
|
+
this.pile.update((existants) => existants.filter((toast) => toast.identifiant !== identifiant));
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* ⚠️ La minuterie s'arrete au survol ET au focus. Sans la seconde, une
|
|
686
|
+
* personne qui atteint le bouton au clavier le voit disparaitre sous son
|
|
687
|
+
* doigt ; sans la premiere, celle qui lit lentement n'a aucun moyen de
|
|
688
|
+
* retenir le message.
|
|
689
|
+
*/
|
|
690
|
+
suspendre(identifiant) {
|
|
691
|
+
this.arreterLaMinuterie(identifiant);
|
|
692
|
+
}
|
|
693
|
+
reprendre(identifiant) {
|
|
694
|
+
const toast = this.pile().find((candidat) => candidat.identifiant === identifiant);
|
|
695
|
+
if (toast !== undefined)
|
|
696
|
+
this.programmer(toast);
|
|
697
|
+
}
|
|
698
|
+
agir(toast) {
|
|
699
|
+
toast.action?.faire();
|
|
700
|
+
this.retirer(toast.identifiant);
|
|
701
|
+
}
|
|
702
|
+
iconeDe(toast) {
|
|
703
|
+
return ICONE[toast.ton];
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* ⚠️ Un message porteur d'action n'est JAMAIS evince. La premiere version
|
|
707
|
+
* gardait les trois plus recents sans distinction : trois confirmations
|
|
708
|
+
* successives faisaient disparaitre un « Annuler » en silence — exactement ce
|
|
709
|
+
* que WCAG 2.2.1 interdit, et ce que le modele promettait d'eviter.
|
|
710
|
+
*
|
|
711
|
+
* Les plus anciens messages sans action partent d'abord. S'il ne reste que des
|
|
712
|
+
* messages porteurs d'action, la borne cede : mieux vaut une pile haute qu'une
|
|
713
|
+
* annulation perdue.
|
|
714
|
+
*/
|
|
715
|
+
borner(pile) {
|
|
716
|
+
const resultat = [...pile];
|
|
717
|
+
while (resultat.length > MAXIMUM_AFFICHE) {
|
|
718
|
+
const indice = resultat.findIndex((toast) => toast.action === undefined);
|
|
719
|
+
if (indice === -1)
|
|
720
|
+
break;
|
|
721
|
+
const [evince] = resultat.splice(indice, 1);
|
|
722
|
+
if (evince !== undefined)
|
|
723
|
+
this.arreterLaMinuterie(evince.identifiant);
|
|
724
|
+
}
|
|
725
|
+
return resultat;
|
|
726
|
+
}
|
|
727
|
+
annoncer(texte, urgence) {
|
|
728
|
+
const region = urgence === 'assertif' ? this.annonceAssertive : this.annoncePolie;
|
|
729
|
+
const precedente = this.minuteriesDAnnonce.get(urgence);
|
|
730
|
+
if (precedente !== undefined)
|
|
731
|
+
clearTimeout(precedente);
|
|
732
|
+
region.set('');
|
|
733
|
+
this.minuteriesDAnnonce.set(urgence, setTimeout(() => {
|
|
734
|
+
region.set(texte);
|
|
735
|
+
this.minuteriesDAnnonce.delete(urgence);
|
|
736
|
+
}, DELAI_D_ANNONCE_MS));
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Un message porteur d'action ne s'efface pas seul : voir `DemandeDeToast`,
|
|
740
|
+
* et WCAG 2.2.1.
|
|
741
|
+
*/
|
|
742
|
+
programmer(toast) {
|
|
743
|
+
if (toast.action !== undefined)
|
|
744
|
+
return;
|
|
745
|
+
const duree = toast.dureeMs === undefined ? DUREE_PAR_DEFAUT_MS : toast.dureeMs;
|
|
746
|
+
if (duree === null)
|
|
747
|
+
return;
|
|
748
|
+
this.arreterLaMinuterie(toast.identifiant);
|
|
749
|
+
this.minuteries.set(toast.identifiant, setTimeout(() => {
|
|
750
|
+
this.retirer(toast.identifiant);
|
|
751
|
+
}, duree));
|
|
752
|
+
}
|
|
753
|
+
arreterLaMinuterie(identifiant) {
|
|
754
|
+
const minuterie = this.minuteries.get(identifiant);
|
|
755
|
+
if (minuterie !== undefined)
|
|
756
|
+
clearTimeout(minuterie);
|
|
757
|
+
this.minuteries.delete(identifiant);
|
|
758
|
+
}
|
|
759
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToastContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
760
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: StToastContainer, isStandalone: true, selector: "st-toast-container", host: { classAttribute: "st-toast-container" }, ngImport: i0, template: "<!-- Regions vivantes PERMANENTES, creees vides avec le conteneur : ce sont\n elles qui annoncent, jamais les messages visibles. -->\n<div class=\"st-toast-container__annonce\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ annoncePolie() }}\n</div>\n<div class=\"st-toast-container__annonce\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n {{ annonceAssertive() }}\n</div>\n\n@for (toast of messages(); track toast.identifiant) {\n <div\n class=\"st-toast\"\n [attr.data-ton]=\"toast.ton\"\n (mouseenter)=\"suspendre(toast.identifiant)\"\n (mouseleave)=\"reprendre(toast.identifiant)\"\n (focusin)=\"suspendre(toast.identifiant)\"\n (focusout)=\"reprendre(toast.identifiant)\"\n >\n <st-icon [nom]=\"iconeDe(toast)\" taille=\"sm\" />\n <p class=\"st-toast__message\">{{ toast.message }}</p>\n\n @if (toast.action; as action) {\n <button stButton variante=\"discret\" taille=\"sm\" type=\"button\" (click)=\"agir(toast)\">\n {{ action.libelle }}\n </button>\n }\n\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton. Le nom accessible est\n impose par le type (`etiquette` est requis) et verifie par axe. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"croix\"\n taille=\"sm\"\n [etiquette]=\"libelleFermeture()\"\n (click)=\"retirer(toast.identifiant)\"\n ></button>\n </div>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{position:fixed;inset-block-end:var(--st-espacement-4);inset-inline:var(--st-espacement-4);z-index:var(--st-plan-notification);display:flex;flex-direction:column;gap:var(--st-espacement-2);align-items:center;pointer-events:none}.st-toast{display:flex;gap:var(--st-espacement-3);align-items:center;inline-size:100%;max-inline-size:28rem;padding:var(--st-espacement-3) var(--st-espacement-4);color:var(--st-couleur-texte);pointer-events:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}.st-toast__message{flex:1 1 auto;min-inline-size:0;margin:0}.st-toast[data-ton=succes] st-icon{color:var(--st-couleur-succes)}.st-toast[data-ton=avertissement] st-icon{color:var(--st-couleur-avertissement)}.st-toast[data-ton=danger] st-icon{color:var(--st-couleur-danger)}.st-toast[data-ton=primaire] st-icon{color:var(--st-couleur-primaire)}@media(forced-colors:active){.st-toast{border-color:CanvasText}}.st-toast-container__annonce{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: StIcon, selector: "st-icon", inputs: ["nom", "taille", "ton", "etiquette"] }, { kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }, { kind: "component", type: StIconButton, selector: "button[stIconButton]", inputs: ["icone", "etiquette", "variante", "taille", "enfonce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
761
|
+
}
|
|
762
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToastContainer, decorators: [{
|
|
763
|
+
type: Component,
|
|
764
|
+
args: [{ selector: 'st-toast-container', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StIcon, StButton, StIconButton], host: { class: 'st-toast-container' }, template: "<!-- Regions vivantes PERMANENTES, creees vides avec le conteneur : ce sont\n elles qui annoncent, jamais les messages visibles. -->\n<div class=\"st-toast-container__annonce\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ annoncePolie() }}\n</div>\n<div class=\"st-toast-container__annonce\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n {{ annonceAssertive() }}\n</div>\n\n@for (toast of messages(); track toast.identifiant) {\n <div\n class=\"st-toast\"\n [attr.data-ton]=\"toast.ton\"\n (mouseenter)=\"suspendre(toast.identifiant)\"\n (mouseleave)=\"reprendre(toast.identifiant)\"\n (focusin)=\"suspendre(toast.identifiant)\"\n (focusout)=\"reprendre(toast.identifiant)\"\n >\n <st-icon [nom]=\"iconeDe(toast)\" taille=\"sm\" />\n <p class=\"st-toast__message\">{{ toast.message }}</p>\n\n @if (toast.action; as action) {\n <button stButton variante=\"discret\" taille=\"sm\" type=\"button\" (click)=\"agir(toast)\">\n {{ action.libelle }}\n </button>\n }\n\n <!-- eslint-disable-next-line @angular-eslint/template/elements-content --\n Le contenu vient du gabarit de stIconButton. Le nom accessible est\n impose par le type (`etiquette` est requis) et verifie par axe. -->\n <button\n stIconButton\n type=\"button\"\n icone=\"croix\"\n taille=\"sm\"\n [etiquette]=\"libelleFermeture()\"\n (click)=\"retirer(toast.identifiant)\"\n ></button>\n </div>\n}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{position:fixed;inset-block-end:var(--st-espacement-4);inset-inline:var(--st-espacement-4);z-index:var(--st-plan-notification);display:flex;flex-direction:column;gap:var(--st-espacement-2);align-items:center;pointer-events:none}.st-toast{display:flex;gap:var(--st-espacement-3);align-items:center;inline-size:100%;max-inline-size:28rem;padding:var(--st-espacement-3) var(--st-espacement-4);color:var(--st-couleur-texte);pointer-events:auto;background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}.st-toast__message{flex:1 1 auto;min-inline-size:0;margin:0}.st-toast[data-ton=succes] st-icon{color:var(--st-couleur-succes)}.st-toast[data-ton=avertissement] st-icon{color:var(--st-couleur-avertissement)}.st-toast[data-ton=danger] st-icon{color:var(--st-couleur-danger)}.st-toast[data-ton=primaire] st-icon{color:var(--st-couleur-primaire)}@media(forced-colors:active){.st-toast{border-color:CanvasText}}.st-toast-container__annonce{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"] }]
|
|
765
|
+
}], ctorParameters: () => [] });
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Messages ephemeres.
|
|
769
|
+
*
|
|
770
|
+
* ```ts
|
|
771
|
+
* const messages = inject(StToast);
|
|
772
|
+
* messages.montrer({ message: 'Virement enregistre', ton: 'succes' });
|
|
773
|
+
* messages.montrer({
|
|
774
|
+
* message: 'Beneficiaire supprime',
|
|
775
|
+
* action: { libelle: 'Annuler', faire: () => this.retablir() },
|
|
776
|
+
* });
|
|
777
|
+
* ```
|
|
778
|
+
*
|
|
779
|
+
* ⚠️ **Un seul conteneur pour toute l'application**, cree a la premiere
|
|
780
|
+
* demande. Deux piles superposees se recouvrent, et la seconde masque la
|
|
781
|
+
* premiere sans que rien ne le signale.
|
|
782
|
+
*
|
|
783
|
+
* ⚠️ **A n'employer que pour ce qui peut etre manque.** Un message ephemere
|
|
784
|
+
* n'est pas une facon d'annoncer une erreur : si l'information compte, elle
|
|
785
|
+
* doit rester a l'ecran — `st-banner` pour la page, `st-inline-message` pour un
|
|
786
|
+
* champ. La regle de tri : si la personne qui n'a pas vu le message est
|
|
787
|
+
* penalisee, ce n'est pas un toast.
|
|
788
|
+
*/
|
|
789
|
+
class StToast {
|
|
790
|
+
overlay = inject(Overlay);
|
|
791
|
+
couche = null;
|
|
792
|
+
conteneur = null;
|
|
793
|
+
constructor() {
|
|
794
|
+
// La couche est liberee avec le service : une micro-SPA federee se demonte,
|
|
795
|
+
// et une couche orpheline resterait posee dans le document de l'hote.
|
|
796
|
+
inject(DestroyRef).onDestroy(() => {
|
|
797
|
+
this.couche?.dispose();
|
|
798
|
+
this.couche = null;
|
|
799
|
+
this.conteneur = null;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/** Affiche un message. Rend son identifiant, pour pouvoir le retirer. */
|
|
803
|
+
montrer(demande) {
|
|
804
|
+
return this.pile().ajouter(demande);
|
|
805
|
+
}
|
|
806
|
+
retirer(identifiant) {
|
|
807
|
+
this.conteneur?.instance.retirer(identifiant);
|
|
808
|
+
}
|
|
809
|
+
pile() {
|
|
810
|
+
if (this.conteneur !== null)
|
|
811
|
+
return this.conteneur.instance;
|
|
812
|
+
// La couche d'overlay du CDK, et non le corps du document : elle est deja
|
|
813
|
+
// au bon plan, et le socle n'a pas a se disputer le `z-index` avec la page.
|
|
814
|
+
this.couche = this.overlay.create({ panelClass: 'st-toast-couche' });
|
|
815
|
+
this.conteneur = this.couche.attach(new ComponentPortal(StToastContainer));
|
|
816
|
+
return this.conteneur.instance;
|
|
817
|
+
}
|
|
818
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
819
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, providedIn: 'root' });
|
|
820
|
+
}
|
|
821
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StToast, decorators: [{
|
|
822
|
+
type: Injectable,
|
|
823
|
+
args: [{ providedIn: 'root' }]
|
|
824
|
+
}], ctorParameters: () => [] });
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Seuils ou le temps restant s'annonce.
|
|
828
|
+
*
|
|
829
|
+
* ⚠️ JAMAIS a chaque seconde : une region qui change toutes les secondes
|
|
830
|
+
* recouvre tout ce que le lecteur d'ecran dit, y compris les deux boutons que la
|
|
831
|
+
* personne cherche justement a atteindre.
|
|
832
|
+
*/
|
|
833
|
+
const SEUILS_D_ANNONCE = [60, 30, 10];
|
|
834
|
+
/** Contenu du dialogue d'avertissement. Ouvert par `st-idle-warning`, jamais directement. */
|
|
835
|
+
class StIdleWarningPanel {
|
|
836
|
+
reference = inject(DialogRef);
|
|
837
|
+
libelles = inject(ST_LIBELLES);
|
|
838
|
+
locale = inject(ST_LOCALE);
|
|
839
|
+
donnees = inject(DIALOG_DATA);
|
|
840
|
+
annonce = new AnnonceLocale(inject(DestroyRef));
|
|
841
|
+
titre = computed(() => this.libelles().sessionBientotExpiree, /* @ts-ignore */
|
|
842
|
+
...(ngDevMode ? [{ debugName: "titre" }] : /* istanbul ignore next */ []));
|
|
843
|
+
message = computed(() => this.libelles().sessionExpireDans(formaterDuree(this.donnees.restant(), this.locale())), /* @ts-ignore */
|
|
844
|
+
...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
|
|
845
|
+
libelleRester = computed(() => this.libelles().resterConnecte, /* @ts-ignore */
|
|
846
|
+
...(ngDevMode ? [{ debugName: "libelleRester" }] : /* istanbul ignore next */ []));
|
|
847
|
+
libelleDeconnecter = computed(() => this.libelles().seDeconnecter, /* @ts-ignore */
|
|
848
|
+
...(ngDevMode ? [{ debugName: "libelleDeconnecter" }] : /* istanbul ignore next */ []));
|
|
849
|
+
constructor() {
|
|
850
|
+
// A l'ouverture, le dialogue d'alerte est lu avec sa description : rien a
|
|
851
|
+
// annoncer de plus. Ensuite, seul le franchissement d'un seuil s'annonce.
|
|
852
|
+
let precedent = null;
|
|
853
|
+
effect(() => {
|
|
854
|
+
const restant = this.donnees.restant();
|
|
855
|
+
const franchi = precedent !== null &&
|
|
856
|
+
SEUILS_D_ANNONCE.some((seuil) => (precedent ?? 0) > seuil && restant <= seuil);
|
|
857
|
+
precedent = restant;
|
|
858
|
+
if (franchi)
|
|
859
|
+
this.annonce.annoncer(this.message());
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
rester() {
|
|
863
|
+
this.reference.close('prolonger');
|
|
864
|
+
}
|
|
865
|
+
deconnecter() {
|
|
866
|
+
this.reference.close('deconnecter');
|
|
867
|
+
}
|
|
868
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarningPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
869
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: StIdleWarningPanel, isStandalone: true, selector: "st-idle-warning-panel", host: { classAttribute: "st-idle-warning-panel" }, ngImport: i0, template: "<st-dialog-title>{{ titre() }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-idle-warning-panel__message\" [id]=\"donnees.identifiantDeDescription\">\n {{ message() }}\n </p>\n <span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n</st-dialog-content>\n\n<!-- L'action destructrice a gauche, la moins destructrice a droite et en\n primaire : c'est elle qui recoit le focus a l'ouverture. -->\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"deconnecter()\">\n {{ libelleDeconnecter() }}\n </button>\n <button\n class=\"st-idle-warning-panel__rester\"\n stButton\n variante=\"primaire\"\n type=\"button\"\n (click)=\"rester()\"\n >\n {{ libelleRester() }}\n </button>\n</st-dialog-actions>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-idle-warning-panel__message{margin:0;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: StButton, selector: "button[stButton]", inputs: ["variante", "taille", "pleineLargeur", "iconeAvant", "iconeApres", "chargement", "indisponible"] }, { kind: "component", type: StDialogTitle, selector: "st-dialog-title" }, { kind: "component", type: StDialogContent, selector: "st-dialog-content", inputs: ["rembourrage"] }, { kind: "component", type: StDialogActions, selector: "st-dialog-actions", inputs: ["alignement"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
870
|
+
}
|
|
871
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarningPanel, decorators: [{
|
|
872
|
+
type: Component,
|
|
873
|
+
args: [{ selector: 'st-idle-warning-panel', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StButton, StDialogTitle, StDialogContent, StDialogActions], host: { class: 'st-idle-warning-panel' }, template: "<st-dialog-title>{{ titre() }}</st-dialog-title>\n\n<st-dialog-content>\n <p class=\"st-idle-warning-panel__message\" [id]=\"donnees.identifiantDeDescription\">\n {{ message() }}\n </p>\n <span class=\"st-vh\" aria-live=\"polite\">{{ annonce.texte() }}</span>\n</st-dialog-content>\n\n<!-- L'action destructrice a gauche, la moins destructrice a droite et en\n primaire : c'est elle qui recoit le focus a l'ouverture. -->\n<st-dialog-actions>\n <button stButton variante=\"secondaire\" type=\"button\" (click)=\"deconnecter()\">\n {{ libelleDeconnecter() }}\n </button>\n <button\n class=\"st-idle-warning-panel__rester\"\n stButton\n variante=\"primaire\"\n type=\"button\"\n (click)=\"rester()\"\n >\n {{ libelleRester() }}\n </button>\n</st-dialog-actions>\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block}.st-idle-warning-panel__message{margin:0;font-variant-numeric:tabular-nums}\n"] }]
|
|
874
|
+
}], ctorParameters: () => [] });
|
|
875
|
+
|
|
876
|
+
let compteur$1 = 0;
|
|
877
|
+
/**
|
|
878
|
+
* Avertissement avant fermeture de session pour inactivite.
|
|
879
|
+
*
|
|
880
|
+
* ```html
|
|
881
|
+
* <st-idle-warning
|
|
882
|
+
* [secondesRestantes]="inactivite.secondesRestantes()"
|
|
883
|
+
* (prolonge)="inactivite.prolonger()"
|
|
884
|
+
* (deconnecte)="session.deconnecter()"
|
|
885
|
+
* />
|
|
886
|
+
* ```
|
|
887
|
+
*
|
|
888
|
+
* ⚠️ **Composant de PRESENTATION.** Il n'injecte pas `StInactivite` : `ui` ne
|
|
889
|
+
* depend pas de `@starterlib/core` (ADR 0003). Il s'ouvre quand
|
|
890
|
+
* `secondesRestantes` cesse d'etre `null`, et se ferme quand il y revient. La
|
|
891
|
+
* decision de fermer la session reste au noyau, qui la prend cote serveur.
|
|
892
|
+
*
|
|
893
|
+
* ⚠️ **Un `alertdialog`, focus sur « Rester connecte ».** Le motif ARIA veut le
|
|
894
|
+
* focus sur l'action la moins destructrice : une touche Entree reflexe doit
|
|
895
|
+
* garder la session, jamais la fermer.
|
|
896
|
+
*
|
|
897
|
+
* ⚠️ **Le clic a cote ne prolonge rien**, pour la raison que donne `StInactivite`
|
|
898
|
+
* du noyau : le dialogue disparaitrait sans que la session ait ete prolongee cote
|
|
899
|
+
* serveur. Il faut repondre. Echap, geste delibere, vaut « Rester connecte ».
|
|
900
|
+
*/
|
|
901
|
+
class StIdleWarning {
|
|
902
|
+
dialogues = inject(StDialog);
|
|
903
|
+
/** `null` : aucun avertissement en cours. */
|
|
904
|
+
secondesRestantes = input(null, /* @ts-ignore */
|
|
905
|
+
...(ngDevMode ? [{ debugName: "secondesRestantes" }] : /* istanbul ignore next */ []));
|
|
906
|
+
prolonge = output();
|
|
907
|
+
deconnecte = output();
|
|
908
|
+
restant = signal(0, /* @ts-ignore */
|
|
909
|
+
...(ngDevMode ? [{ debugName: "restant" }] : /* istanbul ignore next */ []));
|
|
910
|
+
reference = null;
|
|
911
|
+
identifiantDeDescription = `st-inactivite-${(compteur$1 += 1).toString()}`;
|
|
912
|
+
constructor() {
|
|
913
|
+
effect(() => {
|
|
914
|
+
const secondes = this.secondesRestantes();
|
|
915
|
+
if (secondes === null) {
|
|
916
|
+
this.fermerSansReponse();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
this.restant.set(secondes);
|
|
920
|
+
if (this.reference === null)
|
|
921
|
+
this.ouvrir();
|
|
922
|
+
});
|
|
923
|
+
inject(DestroyRef).onDestroy(() => {
|
|
924
|
+
this.fermerSansReponse();
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
ouvrir() {
|
|
928
|
+
const donnees = {
|
|
929
|
+
restant: this.restant.asReadonly(),
|
|
930
|
+
identifiantDeDescription: this.identifiantDeDescription,
|
|
931
|
+
};
|
|
932
|
+
const reference = this.dialogues.ouvrir(StIdleWarningPanel, {
|
|
933
|
+
donnees,
|
|
934
|
+
role: 'alertdialog',
|
|
935
|
+
descriptionDe: this.identifiantDeDescription,
|
|
936
|
+
focusInitial: '.st-idle-warning-panel__rester',
|
|
937
|
+
fondInerte: true,
|
|
938
|
+
});
|
|
939
|
+
this.reference = reference;
|
|
940
|
+
void reference.ferme.then((reponse) => {
|
|
941
|
+
// Fermeture decidee ici — fin du decompte, composant detruit : pas de reponse.
|
|
942
|
+
if (this.reference !== reference)
|
|
943
|
+
return;
|
|
944
|
+
this.reference = null;
|
|
945
|
+
if (reponse === 'deconnecter')
|
|
946
|
+
this.deconnecte.emit();
|
|
947
|
+
// Echap, ou « Rester connecte » : la personne est la.
|
|
948
|
+
else
|
|
949
|
+
this.prolonge.emit();
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
fermerSansReponse() {
|
|
953
|
+
const reference = this.reference;
|
|
954
|
+
this.reference = null;
|
|
955
|
+
reference?.fermer();
|
|
956
|
+
}
|
|
957
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarning, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
958
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StIdleWarning, isStandalone: true, selector: "st-idle-warning", inputs: { secondesRestantes: { classPropertyName: "secondesRestantes", publicName: "secondesRestantes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { prolonge: "prolonge", deconnecte: "deconnecte" }, host: { classAttribute: "st-idle-warning" }, ngImport: i0, template: "", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
959
|
+
}
|
|
960
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StIdleWarning, decorators: [{
|
|
961
|
+
type: Component,
|
|
962
|
+
args: [{ selector: 'st-idle-warning', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-idle-warning' }, template: "" }]
|
|
963
|
+
}], ctorParameters: () => [], propDecorators: { secondesRestantes: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondesRestantes", required: false }] }], prolonge: [{ type: i0.Output, args: ["prolonge"] }], deconnecte: [{ type: i0.Output, args: ["deconnecte"] }] } });
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Bulle visible d'une infobulle. Posee par `stTooltip`, jamais directement.
|
|
967
|
+
*
|
|
968
|
+
* ⚠️ Muette (`aria-hidden`) : le texte est deja lu comme DESCRIPTION du
|
|
969
|
+
* declencheur, par un noeud persistant. L'exposer aussi ici le ferait lire deux
|
|
970
|
+
* fois.
|
|
971
|
+
*/
|
|
972
|
+
class StTooltipPanel {
|
|
973
|
+
texte = input.required(/* @ts-ignore */
|
|
974
|
+
...(ngDevMode ? [{ debugName: "texte" }] : /* istanbul ignore next */ []));
|
|
975
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltipPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
976
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StTooltipPanel, isStandalone: true, selector: "st-tooltip-panel", inputs: { texte: { classPropertyName: "texte", publicName: "texte", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "st-tooltip-panel" }, ngImport: i0, template: "{{ texte() }}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;max-inline-size:18rem;padding:var(--st-espacement-1) var(--st-espacement-2);font-size:var(--st-taille-sm);line-height:var(--st-interligne-normal);color:var(--st-couleur-texte-inverse);pointer-events:auto;background:var(--st-couleur-texte);border-radius:var(--st-rayon-sm);box-shadow:var(--st-ombre-md)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
977
|
+
}
|
|
978
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltipPanel, decorators: [{
|
|
979
|
+
type: Component,
|
|
980
|
+
args: [{ selector: 'st-tooltip-panel', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'st-tooltip-panel', 'aria-hidden': 'true' }, template: "{{ texte() }}\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:block;max-inline-size:18rem;padding:var(--st-espacement-1) var(--st-espacement-2);font-size:var(--st-taille-sm);line-height:var(--st-interligne-normal);color:var(--st-couleur-texte-inverse);pointer-events:auto;background:var(--st-couleur-texte);border-radius:var(--st-rayon-sm);box-shadow:var(--st-ombre-md)}@media(forced-colors:active){:host{border:1px solid CanvasText}}\n"] }]
|
|
981
|
+
}], propDecorators: { texte: [{ type: i0.Input, args: [{ isSignal: true, alias: "texte", required: true }] }] } });
|
|
982
|
+
|
|
983
|
+
const POSITIONS$1 = {
|
|
984
|
+
haut: [
|
|
985
|
+
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -6 },
|
|
986
|
+
{ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 6 },
|
|
987
|
+
],
|
|
988
|
+
bas: [
|
|
989
|
+
{ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 6 },
|
|
990
|
+
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -6 },
|
|
991
|
+
],
|
|
992
|
+
};
|
|
993
|
+
/** Delai au survol : sans lui, balayer une barre d'outils fait clignoter chaque bulle. */
|
|
994
|
+
const DELAI_AU_SURVOL_MS = 300;
|
|
995
|
+
/**
|
|
996
|
+
* Delai avant de masquer quand le pointeur quitte le declencheur : le temps de
|
|
997
|
+
* le poser sur la bulle, qui doit pouvoir etre survolee (WCAG 1.4.13).
|
|
998
|
+
*/
|
|
999
|
+
const DELAI_DE_GRACE_MS = 120;
|
|
1000
|
+
/**
|
|
1001
|
+
* Infobulle : un complement de texte, au survol ET au focus.
|
|
1002
|
+
*
|
|
1003
|
+
* ```html
|
|
1004
|
+
* <button stIconButton icone="corbeille" etiquette="Supprimer" stTooltip="Supprimer le beneficiaire"></button>
|
|
1005
|
+
* ```
|
|
1006
|
+
*
|
|
1007
|
+
* WCAG 1.4.13, les trois exigences, chacune tenue ici :
|
|
1008
|
+
*
|
|
1009
|
+
* - **Refermable** : Echap la masque SANS deplacer le focus, et sans fermer le
|
|
1010
|
+
* dialogue qui la contiendrait — la premiere touche est pour elle.
|
|
1011
|
+
* - **Survolable** : le pointeur peut passer du declencheur a la bulle sans
|
|
1012
|
+
* qu'elle disparaisse.
|
|
1013
|
+
* - **Persistante** : elle reste tant que le survol ou le focus dure.
|
|
1014
|
+
*
|
|
1015
|
+
* ⚠️ **Le texte est une DESCRIPTION posee des le depart**, par un noeud
|
|
1016
|
+
* persistant. Un `aria-describedby` qui viserait la bulle elle-meme pointerait
|
|
1017
|
+
* vers un element absent au moment ou le focus arrive : le lecteur d'ecran,
|
|
1018
|
+
* qui calcule la description a cet instant, ne lirait rien.
|
|
1019
|
+
*
|
|
1020
|
+
* ⚠️ **Jamais d'information exclusive, jamais de contenu interactif.** Sur un
|
|
1021
|
+
* ecran tactile, il n'y a ni survol ni focus visible : l'infobulle n'existe pas.
|
|
1022
|
+
* Ce qu'elle dit doit pouvoir manquer. Un lien ou un bouton dedans serait
|
|
1023
|
+
* inatteignable — c'est un `st-popover` qu'il faut alors.
|
|
1024
|
+
*/
|
|
1025
|
+
class StTooltip {
|
|
1026
|
+
element = inject(ElementRef).nativeElement;
|
|
1027
|
+
overlay = inject(Overlay);
|
|
1028
|
+
descripteur = inject(AriaDescriber);
|
|
1029
|
+
document = inject(DOCUMENT);
|
|
1030
|
+
stTooltip = input.required(/* @ts-ignore */
|
|
1031
|
+
...(ngDevMode ? [{ debugName: "stTooltip" }] : /* istanbul ignore next */ []));
|
|
1032
|
+
positionDInfobulle = input('haut', /* @ts-ignore */
|
|
1033
|
+
...(ngDevMode ? [{ debugName: "positionDInfobulle" }] : /* istanbul ignore next */ []));
|
|
1034
|
+
couche = null;
|
|
1035
|
+
minuterie = null;
|
|
1036
|
+
description = null;
|
|
1037
|
+
echap = (evenement) => {
|
|
1038
|
+
if (evenement.key !== 'Escape' || this.couche === null)
|
|
1039
|
+
return;
|
|
1040
|
+
// Echap est pour la bulle d'abord : sans cet arret, il fermerait aussi le
|
|
1041
|
+
// dialogue qui la contient, et la personne perdrait bien plus qu'une bulle.
|
|
1042
|
+
evenement.stopPropagation();
|
|
1043
|
+
this.masquer();
|
|
1044
|
+
};
|
|
1045
|
+
constructor() {
|
|
1046
|
+
effect(() => {
|
|
1047
|
+
const texte = this.stTooltip().trim();
|
|
1048
|
+
this.retirerLaDescription();
|
|
1049
|
+
if (texte === '')
|
|
1050
|
+
return;
|
|
1051
|
+
this.descripteur.describe(this.element, texte);
|
|
1052
|
+
this.description = texte;
|
|
1053
|
+
});
|
|
1054
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1055
|
+
this.masquer();
|
|
1056
|
+
this.retirerLaDescription();
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
auSurvol() {
|
|
1060
|
+
this.programmer(() => {
|
|
1061
|
+
this.afficher();
|
|
1062
|
+
}, DELAI_AU_SURVOL_MS);
|
|
1063
|
+
}
|
|
1064
|
+
auDepartDuPointeur() {
|
|
1065
|
+
this.programmer(() => {
|
|
1066
|
+
this.masquer();
|
|
1067
|
+
}, DELAI_DE_GRACE_MS);
|
|
1068
|
+
}
|
|
1069
|
+
afficher() {
|
|
1070
|
+
this.annulerLaMinuterie();
|
|
1071
|
+
const texte = this.stTooltip().trim();
|
|
1072
|
+
if (texte === '' || this.couche !== null)
|
|
1073
|
+
return;
|
|
1074
|
+
this.couche = this.overlay.create({
|
|
1075
|
+
positionStrategy: this.overlay
|
|
1076
|
+
.position()
|
|
1077
|
+
.flexibleConnectedTo(this.element)
|
|
1078
|
+
.withPositions(POSITIONS$1[this.positionDInfobulle()])
|
|
1079
|
+
.withPush(false),
|
|
1080
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
1081
|
+
panelClass: 'st-tooltip-couche',
|
|
1082
|
+
});
|
|
1083
|
+
const bulle = this.couche.attach(new ComponentPortal(StTooltipPanel));
|
|
1084
|
+
bulle.setInput('texte', texte);
|
|
1085
|
+
const noeud = bulle.location.nativeElement;
|
|
1086
|
+
noeud.addEventListener('mouseenter', () => {
|
|
1087
|
+
this.annulerLaMinuterie();
|
|
1088
|
+
});
|
|
1089
|
+
noeud.addEventListener('mouseleave', () => {
|
|
1090
|
+
this.auDepartDuPointeur();
|
|
1091
|
+
});
|
|
1092
|
+
// En capture : l'infobulle entend Echap avant le dialogue qui la contient.
|
|
1093
|
+
this.document.addEventListener('keydown', this.echap, true);
|
|
1094
|
+
}
|
|
1095
|
+
masquer() {
|
|
1096
|
+
this.annulerLaMinuterie();
|
|
1097
|
+
if (this.couche === null)
|
|
1098
|
+
return;
|
|
1099
|
+
this.document.removeEventListener('keydown', this.echap, true);
|
|
1100
|
+
this.couche.dispose();
|
|
1101
|
+
this.couche = null;
|
|
1102
|
+
}
|
|
1103
|
+
programmer(action, delai) {
|
|
1104
|
+
this.annulerLaMinuterie();
|
|
1105
|
+
this.minuterie = setTimeout(action, delai);
|
|
1106
|
+
}
|
|
1107
|
+
annulerLaMinuterie() {
|
|
1108
|
+
if (this.minuterie !== null)
|
|
1109
|
+
clearTimeout(this.minuterie);
|
|
1110
|
+
this.minuterie = null;
|
|
1111
|
+
}
|
|
1112
|
+
retirerLaDescription() {
|
|
1113
|
+
if (this.description === null)
|
|
1114
|
+
return;
|
|
1115
|
+
this.descripteur.removeDescription(this.element, this.description);
|
|
1116
|
+
this.description = null;
|
|
1117
|
+
}
|
|
1118
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltip, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1119
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StTooltip, isStandalone: true, selector: "[stTooltip]", inputs: { stTooltip: { classPropertyName: "stTooltip", publicName: "stTooltip", isSignal: true, isRequired: true, transformFunction: null }, positionDInfobulle: { classPropertyName: "positionDInfobulle", publicName: "positionDInfobulle", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "auSurvol()", "mouseleave": "auDepartDuPointeur()", "focusin": "afficher()", "focusout": "masquer()" } }, ngImport: i0 });
|
|
1120
|
+
}
|
|
1121
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StTooltip, decorators: [{
|
|
1122
|
+
type: Directive,
|
|
1123
|
+
args: [{
|
|
1124
|
+
selector: '[stTooltip]',
|
|
1125
|
+
host: {
|
|
1126
|
+
'(mouseenter)': 'auSurvol()',
|
|
1127
|
+
'(mouseleave)': 'auDepartDuPointeur()',
|
|
1128
|
+
'(focusin)': 'afficher()',
|
|
1129
|
+
'(focusout)': 'masquer()',
|
|
1130
|
+
},
|
|
1131
|
+
}]
|
|
1132
|
+
}], ctorParameters: () => [], propDecorators: { stTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "stTooltip", required: true }] }], positionDInfobulle: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionDInfobulle", required: false }] }] } });
|
|
1133
|
+
|
|
1134
|
+
const POSITIONS = {
|
|
1135
|
+
debut: [
|
|
1136
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 6 },
|
|
1137
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -6 },
|
|
1138
|
+
],
|
|
1139
|
+
fin: [
|
|
1140
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 6 },
|
|
1141
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -6 },
|
|
1142
|
+
],
|
|
1143
|
+
};
|
|
1144
|
+
const FOCALISABLES = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), ' +
|
|
1145
|
+
'textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1146
|
+
let compteur = 0;
|
|
1147
|
+
/**
|
|
1148
|
+
* Couche NON modale attachee a un declencheur : filtres rapides, detail d'une
|
|
1149
|
+
* ligne, petit formulaire.
|
|
1150
|
+
*
|
|
1151
|
+
* ```html
|
|
1152
|
+
* <button stButton [stPopoverTrigger]="filtres">Filtres</button>
|
|
1153
|
+
* <st-popover #filtres etiquette="Filtres des operations">…</st-popover>
|
|
1154
|
+
* ```
|
|
1155
|
+
*
|
|
1156
|
+
* Distinct de l'infobulle, qui ne contient que du texte, et du dialogue, qui
|
|
1157
|
+
* piege le focus et met la page en retrait.
|
|
1158
|
+
*
|
|
1159
|
+
* ⚠️ **Non modale, donc aucun piege.** La tabulation peut en sortir, et la page
|
|
1160
|
+
* reste utilisable. Mais une couche que le focus a quittee flotterait sans lien
|
|
1161
|
+
* avec ce qu'on fait : elle se ferme quand le focus part ailleurs que sur elle
|
|
1162
|
+
* ou son declencheur.
|
|
1163
|
+
*
|
|
1164
|
+
* ⚠️ **Deux fermetures, deux destinations du focus.** Echap rend le focus au
|
|
1165
|
+
* declencheur : la personne n'a rien choisi d'autre. Un clic a l'exterieur le
|
|
1166
|
+
* laisse la ou il a ete pose : le lui reprendre annulerait ce clic.
|
|
1167
|
+
*
|
|
1168
|
+
* Meme structure que `st-menu` : le declencheur DESIGNE la couche, et l'element
|
|
1169
|
+
* est DEPLACE dans la couche d'overlay a l'ouverture, avec ses liaisons et son
|
|
1170
|
+
* etat.
|
|
1171
|
+
*/
|
|
1172
|
+
class StPopover {
|
|
1173
|
+
element = inject(ElementRef);
|
|
1174
|
+
overlay = inject(Overlay);
|
|
1175
|
+
/** Nom accessible : sans lui, la couche s'annonce « dialogue » et rien d'autre. */
|
|
1176
|
+
etiquette = input.required(/* @ts-ignore */
|
|
1177
|
+
...(ngDevMode ? [{ debugName: "etiquette" }] : /* istanbul ignore next */ []));
|
|
1178
|
+
alignement = input('debut', /* @ts-ignore */
|
|
1179
|
+
...(ngDevMode ? [{ debugName: "alignement" }] : /* istanbul ignore next */ []));
|
|
1180
|
+
identifiant = `st-popover-${(compteur += 1).toString()}`;
|
|
1181
|
+
ouvertInterne = signal(false, /* @ts-ignore */
|
|
1182
|
+
...(ngDevMode ? [{ debugName: "ouvertInterne" }] : /* istanbul ignore next */ []));
|
|
1183
|
+
ouvert = this.ouvertInterne.asReadonly();
|
|
1184
|
+
couche = null;
|
|
1185
|
+
declencheur = null;
|
|
1186
|
+
constructor() {
|
|
1187
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1188
|
+
this.detruireLaCouche();
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
basculerDepuis(declencheur) {
|
|
1192
|
+
if (this.ouvertInterne())
|
|
1193
|
+
this.fermer();
|
|
1194
|
+
else
|
|
1195
|
+
this.ouvrirDepuis(declencheur);
|
|
1196
|
+
}
|
|
1197
|
+
ouvrirDepuis(declencheur) {
|
|
1198
|
+
if (this.ouvertInterne())
|
|
1199
|
+
return;
|
|
1200
|
+
this.declencheur = declencheur;
|
|
1201
|
+
this.couche = this.overlay.create({
|
|
1202
|
+
positionStrategy: this.overlay
|
|
1203
|
+
.position()
|
|
1204
|
+
.flexibleConnectedTo(declencheur)
|
|
1205
|
+
.withPositions(POSITIONS[this.alignement()])
|
|
1206
|
+
.withPush(true),
|
|
1207
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
1208
|
+
});
|
|
1209
|
+
this.couche.attach(new DomPortal(this.element));
|
|
1210
|
+
this.ouvertInterne.set(true);
|
|
1211
|
+
this.couche.outsidePointerEvents().subscribe((evenement) => {
|
|
1212
|
+
// Le clic sur le declencheur est deja une bascule : le traiter aussi ici
|
|
1213
|
+
// rouvrirait la couche aussitot fermee.
|
|
1214
|
+
if (declencheur.contains(evenement.target))
|
|
1215
|
+
return;
|
|
1216
|
+
this.fermer({ rendreLeFocus: false });
|
|
1217
|
+
});
|
|
1218
|
+
// Le focus entre dans la couche : sur son premier controle s'il en a, sinon
|
|
1219
|
+
// sur la couche elle-meme, pour que son nom soit lu.
|
|
1220
|
+
const hote = this.element.nativeElement;
|
|
1221
|
+
const premier = hote.querySelector(FOCALISABLES);
|
|
1222
|
+
(premier ?? hote).focus();
|
|
1223
|
+
}
|
|
1224
|
+
fermer(options = {}) {
|
|
1225
|
+
if (!this.ouvertInterne())
|
|
1226
|
+
return;
|
|
1227
|
+
this.ouvertInterne.set(false);
|
|
1228
|
+
this.detruireLaCouche();
|
|
1229
|
+
const declencheur = this.declencheur;
|
|
1230
|
+
this.declencheur = null;
|
|
1231
|
+
if (options.rendreLeFocus !== false)
|
|
1232
|
+
declencheur?.focus();
|
|
1233
|
+
}
|
|
1234
|
+
auClavier(evenement) {
|
|
1235
|
+
if (evenement.key !== 'Escape')
|
|
1236
|
+
return;
|
|
1237
|
+
evenement.preventDefault();
|
|
1238
|
+
evenement.stopPropagation();
|
|
1239
|
+
this.fermer();
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* ⚠️ L'element actif est lu APRES la transition de focus, et non dans
|
|
1243
|
+
* `relatedTarget`. Ce dernier est vide quand le focus part vers un element
|
|
1244
|
+
* sans focus, et n'est pas renseigne partout ; pendant la transition elle-meme,
|
|
1245
|
+
* l'element actif est encore le corps du document.
|
|
1246
|
+
*/
|
|
1247
|
+
aLaPerteDuFocus() {
|
|
1248
|
+
setTimeout(() => {
|
|
1249
|
+
if (!this.ouvertInterne())
|
|
1250
|
+
return;
|
|
1251
|
+
const actif = this.element.nativeElement.ownerDocument.activeElement;
|
|
1252
|
+
// Le corps du document : le focus a quitte la page, ou un clic sans cible
|
|
1253
|
+
// focalisable — deja traite comme clic a l'exterieur par la couche.
|
|
1254
|
+
if (actif === null || actif === actif.ownerDocument.body)
|
|
1255
|
+
return;
|
|
1256
|
+
if (this.element.nativeElement.contains(actif))
|
|
1257
|
+
return;
|
|
1258
|
+
if (this.declencheur?.contains(actif) === true)
|
|
1259
|
+
return;
|
|
1260
|
+
this.fermer({ rendreLeFocus: false });
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
detruireLaCouche() {
|
|
1264
|
+
this.couche?.dispose();
|
|
1265
|
+
this.couche = null;
|
|
1266
|
+
}
|
|
1267
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1268
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: StPopover, isStandalone: true, selector: "st-popover", inputs: { etiquette: { classPropertyName: "etiquette", publicName: "etiquette", isSignal: true, isRequired: true, transformFunction: null }, alignement: { classPropertyName: "alignement", publicName: "alignement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "dialog", "tabindex": "-1" }, listeners: { "keydown": "auClavier($event)", "focusout": "aLaPerteDuFocus()" }, properties: { "id": "identifiant", "attr.aria-label": "etiquette()", "attr.data-ouvert": "ouvert() ? \"\" : null" }, classAttribute: "st-popover" }, ngImport: i0, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:none}:host([data-ouvert]){display:block;max-inline-size:min(24rem,100vw - 2rem);padding:var(--st-espacement-4);color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}:host(:focus){outline:none}@media(forced-colors:active){:host([data-ouvert]){border-color:CanvasText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1269
|
+
}
|
|
1270
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopover, decorators: [{
|
|
1271
|
+
type: Component,
|
|
1272
|
+
args: [{ selector: 'st-popover', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1273
|
+
class: 'st-popover',
|
|
1274
|
+
role: 'dialog',
|
|
1275
|
+
tabindex: '-1',
|
|
1276
|
+
'[id]': 'identifiant',
|
|
1277
|
+
'[attr.aria-label]': 'etiquette()',
|
|
1278
|
+
'[attr.data-ouvert]': 'ouvert() ? "" : null',
|
|
1279
|
+
'(keydown)': 'auClavier($event)',
|
|
1280
|
+
'(focusout)': 'aLaPerteDuFocus()',
|
|
1281
|
+
}, template: "<ng-content />\n", styles: [".st-vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([hidden]){display:none!important}:host{display:none}:host([data-ouvert]){display:block;max-inline-size:min(24rem,100vw - 2rem);padding:var(--st-espacement-4);color:var(--st-couleur-texte);background:var(--st-couleur-surface-elevee);border:1px solid var(--st-couleur-bordure);border-radius:var(--st-rayon-md);box-shadow:var(--st-ombre-lg)}:host(:focus){outline:none}@media(forced-colors:active){:host([data-ouvert]){border-color:CanvasText}}\n"] }]
|
|
1282
|
+
}], ctorParameters: () => [], propDecorators: { etiquette: [{ type: i0.Input, args: [{ isSignal: true, alias: "etiquette", required: true }] }], alignement: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignement", required: false }] }] } });
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Declencheur d'un `st-popover`.
|
|
1286
|
+
*
|
|
1287
|
+
* ⚠️ `aria-controls` vise la couche meme fermee : le lien entre les deux existe
|
|
1288
|
+
* pour les technologies d'assistance avant l'ouverture, et `aria-expanded` dit
|
|
1289
|
+
* son etat.
|
|
1290
|
+
*/
|
|
1291
|
+
class StPopoverTrigger {
|
|
1292
|
+
element = inject(ElementRef).nativeElement;
|
|
1293
|
+
stPopoverTrigger = input.required(/* @ts-ignore */
|
|
1294
|
+
...(ngDevMode ? [{ debugName: "stPopoverTrigger" }] : /* istanbul ignore next */ []));
|
|
1295
|
+
basculer() {
|
|
1296
|
+
this.stPopoverTrigger().basculerDepuis(this.element);
|
|
1297
|
+
}
|
|
1298
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopoverTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1299
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StPopoverTrigger, isStandalone: true, selector: "[stPopoverTrigger]", inputs: { stPopoverTrigger: { classPropertyName: "stPopoverTrigger", publicName: "stPopoverTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "click": "basculer()" }, properties: { "attr.aria-expanded": "stPopoverTrigger().ouvert()", "attr.aria-controls": "stPopoverTrigger().identifiant" } }, ngImport: i0 });
|
|
1300
|
+
}
|
|
1301
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StPopoverTrigger, decorators: [{
|
|
1302
|
+
type: Directive,
|
|
1303
|
+
args: [{
|
|
1304
|
+
selector: '[stPopoverTrigger]',
|
|
1305
|
+
host: {
|
|
1306
|
+
'aria-haspopup': 'dialog',
|
|
1307
|
+
'[attr.aria-expanded]': 'stPopoverTrigger().ouvert()',
|
|
1308
|
+
'[attr.aria-controls]': 'stPopoverTrigger().identifiant',
|
|
1309
|
+
'(click)': 'basculer()',
|
|
1310
|
+
},
|
|
1311
|
+
}]
|
|
1312
|
+
}], propDecorators: { stPopoverTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "stPopoverTrigger", required: true }] }] } });
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* @starterlib/ui/surfaces — ce qui se pose AU-DESSUS de la page.
|
|
1316
|
+
*
|
|
1317
|
+
* ⚠️ Un point d'entree separe pour une raison de dependance, et non de rangement :
|
|
1318
|
+
* tout ce qui vit ici traine `@angular/cdk/overlay` et `@angular/cdk/portal`.
|
|
1319
|
+
* Laisse dans le tronc, le CDK entrerait dans le train de singletons de CHAQUE
|
|
1320
|
+
* application federee, y compris celles qui n'affichent ni menu ni dialogue.
|
|
1321
|
+
*
|
|
1322
|
+
* Le decoupage suit donc les pairs lourds, pas les familles de composants : un
|
|
1323
|
+
* decoupage par famille multiplierait les points d'entree a tenir sans rien
|
|
1324
|
+
* retirer de plus au chargement.
|
|
1325
|
+
*
|
|
1326
|
+
* ⚠️ En federation, ce point d'entree se partage SEPAREMENT de
|
|
1327
|
+
* `@starterlib/ui` : c'est un specificateur de module distinct.
|
|
1328
|
+
*
|
|
1329
|
+
* ```ts
|
|
1330
|
+
* import { StMenu, StDialog } from '@starterlib/ui/surfaces';
|
|
1331
|
+
* ```
|
|
1332
|
+
*/
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Generated bundle index. Do not edit.
|
|
1336
|
+
*/
|
|
1337
|
+
|
|
1338
|
+
export { IDENTIFIANT_DU_TITRE, StConfirmDialog, StDialog, StDialogActions, StDialogContainer, StDialogContent, StDialogTitle, StIdleWarning, StIdleWarningPanel, StMenu, StMenuItem, StMenuTrigger, StPopover, StPopoverTrigger, StToast, StToastContainer, StTooltip, StTooltipPanel };
|
|
1339
|
+
//# sourceMappingURL=starterlib-ui-surfaces.mjs.map
|