@starterlib/ui 0.2.0 → 0.4.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 +83 -16
- package/assets/icones/sprite.svg +3 -0
- package/fesm2022/starterlib-ui-formulaires.mjs +446 -0
- package/fesm2022/starterlib-ui-formulaires.mjs.map +1 -0
- package/fesm2022/starterlib-ui-interactions.mjs +536 -0
- package/fesm2022/starterlib-ui-interactions.mjs.map +1 -0
- package/fesm2022/starterlib-ui-surfaces.mjs +1106 -11
- package/fesm2022/starterlib-ui-surfaces.mjs.map +1 -1
- package/fesm2022/starterlib-ui.mjs +2719 -105
- package/fesm2022/starterlib-ui.mjs.map +1 -1
- package/package.json +24 -3
- package/types/starterlib-ui-formulaires.d.ts +198 -0
- package/types/starterlib-ui-interactions.d.ts +269 -0
- package/types/starterlib-ui-surfaces.d.ts +607 -16
- package/types/starterlib-ui.d.ts +1598 -24
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Overlay } from '@angular/cdk/overlay';
|
|
2
|
-
import { DomPortal } from '@angular/cdk/portal';
|
|
2
|
+
import { DomPortal, CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { inject, ElementRef, input, signal, DestroyRef, ChangeDetectionStrategy, Component, Directive } from '@angular/core';
|
|
4
|
+
import { inject, ElementRef, input, signal, DestroyRef, ChangeDetectionStrategy, Component, Directive, InjectionToken, afterNextRender, computed, Injectable, effect, output } from '@angular/core';
|
|
5
5
|
import * as i1 from '@angular/aria/menu';
|
|
6
6
|
import { Menu, MenuItem } from '@angular/aria/menu';
|
|
7
|
-
import { StIcon } from '@starterlib/ui';
|
|
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';
|
|
8
14
|
|
|
9
|
-
const POSITIONS = {
|
|
15
|
+
const POSITIONS$2 = {
|
|
10
16
|
debut: [
|
|
11
17
|
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
12
18
|
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
@@ -76,7 +82,7 @@ class StMenu {
|
|
|
76
82
|
positionStrategy: this.overlay
|
|
77
83
|
.position()
|
|
78
84
|
.flexibleConnectedTo(declencheur)
|
|
79
|
-
.withPositions(POSITIONS[this.alignement()])
|
|
85
|
+
.withPositions(POSITIONS$2[this.alignement()])
|
|
80
86
|
.withPush(true),
|
|
81
87
|
// Le menu suit son declencheur au defilement : le figer le laisserait
|
|
82
88
|
// seul au milieu de l'ecran, sans lien visible avec ce qu'il commande.
|
|
@@ -125,7 +131,7 @@ class StMenu {
|
|
|
125
131
|
this.couche = null;
|
|
126
132
|
}
|
|
127
133
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
128
|
-
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: [":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 });
|
|
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 });
|
|
129
135
|
}
|
|
130
136
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenu, decorators: [{
|
|
131
137
|
type: Component,
|
|
@@ -140,7 +146,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
140
146
|
'[attr.aria-label]': 'etiquette()',
|
|
141
147
|
'[attr.data-ouvert]': 'ouvert() ? "" : null',
|
|
142
148
|
'(keydown)': 'auClavier($event)',
|
|
143
|
-
}, template: "<ng-content />\n", styles: [":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"] }]
|
|
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"] }]
|
|
144
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 }] }] } });
|
|
145
151
|
|
|
146
152
|
/**
|
|
@@ -163,7 +169,7 @@ class StMenuItem {
|
|
|
163
169
|
ton = input('neutre', /* @ts-ignore */
|
|
164
170
|
...(ngDevMode ? [{ debugName: "ton" }] : /* istanbul ignore next */ []));
|
|
165
171
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
166
|
-
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{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 });
|
|
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 });
|
|
167
173
|
}
|
|
168
174
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StMenuItem, decorators: [{
|
|
169
175
|
type: Component,
|
|
@@ -173,7 +179,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
173
179
|
class: 'st-menu-item',
|
|
174
180
|
type: 'button',
|
|
175
181
|
'[attr.data-ton]': 'ton()',
|
|
176
|
-
}, 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{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"] }]
|
|
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"] }]
|
|
177
183
|
}], propDecorators: { icone: [{ type: i0.Input, args: [{ isSignal: true, alias: "icone", required: false }] }], ton: [{ type: i0.Input, args: [{ isSignal: true, alias: "ton", required: false }] }] } });
|
|
178
184
|
|
|
179
185
|
/**
|
|
@@ -219,6 +225,1092 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
219
225
|
}]
|
|
220
226
|
}], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "menu", required: true }] }] } });
|
|
221
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
|
+
|
|
222
1314
|
/**
|
|
223
1315
|
* @starterlib/ui/surfaces — ce qui se pose AU-DESSUS de la page.
|
|
224
1316
|
*
|
|
@@ -231,8 +1323,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
231
1323
|
* decoupage par famille multiplierait les points d'entree a tenir sans rien
|
|
232
1324
|
* retirer de plus au chargement.
|
|
233
1325
|
*
|
|
1326
|
+
* ⚠️ En federation, ce point d'entree se partage SEPAREMENT de
|
|
1327
|
+
* `@starterlib/ui` : c'est un specificateur de module distinct.
|
|
1328
|
+
*
|
|
234
1329
|
* ```ts
|
|
235
|
-
* import { StMenu,
|
|
1330
|
+
* import { StMenu, StDialog } from '@starterlib/ui/surfaces';
|
|
236
1331
|
* ```
|
|
237
1332
|
*/
|
|
238
1333
|
|
|
@@ -240,5 +1335,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
240
1335
|
* Generated bundle index. Do not edit.
|
|
241
1336
|
*/
|
|
242
1337
|
|
|
243
|
-
export { StMenu, StMenuItem, StMenuTrigger };
|
|
1338
|
+
export { IDENTIFIANT_DU_TITRE, StConfirmDialog, StDialog, StDialogActions, StDialogContainer, StDialogContent, StDialogTitle, StIdleWarning, StIdleWarningPanel, StMenu, StMenuItem, StMenuTrigger, StPopover, StPopoverTrigger, StToast, StToastContainer, StTooltip, StTooltipPanel };
|
|
244
1339
|
//# sourceMappingURL=starterlib-ui-surfaces.mjs.map
|