@ktortu/aaa 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +151 -0
- package/button/button-tokens.css +152 -0
- package/button/button.css +319 -0
- package/card/card-tokens.css +49 -0
- package/card/card.css +200 -0
- package/cdk/styles/foundation.css +83 -0
- package/cdk/styles/tabs.css +276 -0
- package/dialog/dialog.css +350 -0
- package/fesm2022/ktortu-aaa-button.mjs +128 -0
- package/fesm2022/ktortu-aaa-button.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-card.mjs +209 -0
- package/fesm2022/ktortu-aaa-card.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-cdk.mjs +183 -0
- package/fesm2022/ktortu-aaa-cdk.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-dialog.mjs +512 -0
- package/fesm2022/ktortu-aaa-dialog.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-forms.mjs +3215 -0
- package/fesm2022/ktortu-aaa-forms.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-menu.mjs +315 -0
- package/fesm2022/ktortu-aaa-menu.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-tabs.mjs +79 -0
- package/fesm2022/ktortu-aaa-tabs.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-tooltip.mjs +356 -0
- package/fesm2022/ktortu-aaa-tooltip.mjs.map +1 -0
- package/fesm2022/ktortu-aaa.mjs +17 -0
- package/fesm2022/ktortu-aaa.mjs.map +1 -0
- package/forms/checkbox/checkbox-group.css +55 -0
- package/forms/checkbox/checkbox.css +216 -0
- package/forms/chips/chip-list.css +70 -0
- package/forms/chips/chip.css +92 -0
- package/forms/chips/tokens.css +102 -0
- package/forms/field/field.css +87 -0
- package/forms/multi-select/multi-select.css +136 -0
- package/forms/radio/radio-group.css +55 -0
- package/forms/radio/radio.css +165 -0
- package/forms/styles/field-box.css +171 -0
- package/forms/styles/select-panel.css +464 -0
- package/forms/styles/tokens.css +67 -0
- package/forms/switch/switch.css +188 -0
- package/menu/menu-tokens.css +58 -0
- package/menu/menu.css +224 -0
- package/package.json +96 -0
- package/styles/button.css +6 -0
- package/styles/card.css +6 -0
- package/styles/dialog.css +6 -0
- package/styles/forms.css +13 -0
- package/styles/foundation.css +7 -0
- package/styles/menu.css +6 -0
- package/styles/styles.css +24 -0
- package/styles/tabs.css +5 -0
- package/styles/tooltip.css +5 -0
- package/themes/theme-ant.css +44 -0
- package/themes/theme-architecte.css +83 -0
- package/themes/theme-aurora.css +97 -0
- package/themes/theme-bootstrap.css +46 -0
- package/themes/theme-carbon.css +49 -0
- package/themes/theme-catppuccin.css +66 -0
- package/themes/theme-cyberpunk.css +211 -0
- package/themes/theme-fluent.css +45 -0
- package/themes/theme-material-you.css +74 -0
- package/themes/theme-material.css +48 -0
- package/themes/theme-primer.css +46 -0
- package/themes/theme-vegetal.css +78 -0
- package/tooltip/tooltip.css +129 -0
- package/types/ktortu-aaa-button.d.ts +70 -0
- package/types/ktortu-aaa-card.d.ts +143 -0
- package/types/ktortu-aaa-cdk.d.ts +110 -0
- package/types/ktortu-aaa-dialog.d.ts +286 -0
- package/types/ktortu-aaa-forms.d.ts +1574 -0
- package/types/ktortu-aaa-menu.d.ts +171 -0
- package/types/ktortu-aaa-tabs.d.ts +27 -0
- package/types/ktortu-aaa-tooltip.d.ts +90 -0
- package/types/ktortu-aaa.d.ts +8 -0
|
@@ -0,0 +1,3215 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, computed, ElementRef, Directive, input, output, contentChild, viewChild, forwardRef, ChangeDetectionStrategy, Component, model, booleanAttribute, TemplateRef, effect, NgZone, PLATFORM_ID, DestroyRef, signal, untracked, isDevMode, Injector, ChangeDetectorRef, viewChildren, afterNextRender, Injectable, LOCALE_ID, Pipe } from '@angular/core';
|
|
3
|
+
import { KtTooltip } from '@ktortu/aaa/tooltip';
|
|
4
|
+
import { KtIdGenerator, KtViewport, createKtSheetDrag } from '@ktortu/aaa/cdk';
|
|
5
|
+
import { NgTemplateOutlet, DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
6
|
+
import { transformedValue } from '@angular/forms/signals';
|
|
7
|
+
import { Combobox, ComboboxPopup, ComboboxWidget } from '@angular/aria/combobox';
|
|
8
|
+
import { Listbox, Option } from '@angular/aria/listbox';
|
|
9
|
+
|
|
10
|
+
/** Défaut : afficher l'erreur une fois le champ quitté (blur). */
|
|
11
|
+
const defaultKtFieldErrorMatcher = (state) => state.invalid && state.touched;
|
|
12
|
+
const KT_FIELD_CONFIG = new InjectionToken('KT_FIELD_CONFIG');
|
|
13
|
+
const KT_FIELD = new InjectionToken('KT_FIELD');
|
|
14
|
+
|
|
15
|
+
/** Marque le contrôle (natif ou custom) d'un champ pour que `Field` y câble l'a11y
|
|
16
|
+
(id, aria-describedby, aria-invalid, aria-required). */
|
|
17
|
+
class KtFieldControl {
|
|
18
|
+
parent = inject(KT_FIELD, { optional: true });
|
|
19
|
+
id = computed(() => this.parent?.baseId() ?? null, /* @ts-ignore */
|
|
20
|
+
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
21
|
+
describedBy = computed(() => this.parent?.describedBy() ?? null, /* @ts-ignore */
|
|
22
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
23
|
+
invalid = computed(() => this.parent?.invalid() ?? false, /* @ts-ignore */
|
|
24
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
25
|
+
required = computed(() => this.parent?.required() ?? false, /* @ts-ignore */
|
|
26
|
+
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
27
|
+
element = inject(ElementRef).nativeElement;
|
|
28
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtFieldControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
29
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtFieldControl, isStandalone: true, selector: "[ktFieldControl]", host: { properties: { "id": "id()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "invalid() ? \"true\" : null", "attr.aria-required": "required() ? \"true\" : null" } }, ngImport: i0 });
|
|
30
|
+
}
|
|
31
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtFieldControl, decorators: [{
|
|
32
|
+
type: Directive,
|
|
33
|
+
args: [{
|
|
34
|
+
selector: '[ktFieldControl]',
|
|
35
|
+
host: {
|
|
36
|
+
'[id]': 'id()',
|
|
37
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
38
|
+
'[attr.aria-invalid]': 'invalid() ? "true" : null',
|
|
39
|
+
'[attr.aria-required]': 'required() ? "true" : null',
|
|
40
|
+
},
|
|
41
|
+
}]
|
|
42
|
+
}] });
|
|
43
|
+
|
|
44
|
+
/** Chrome de champ agnostique du système de forms : label + contrôle projeté + hint + erreur,
|
|
45
|
+
et centralisation du câblage a11y. Nourri par des inputs simples.
|
|
46
|
+
|
|
47
|
+
@example
|
|
48
|
+
```html
|
|
49
|
+
<kt-field label="E-mail" hint="Nous ne partagerons jamais votre adresse." [invalid]="emailInvalid()" [errors]="emailErrors()" required>
|
|
50
|
+
<input ktFieldControl type="email" [(ngModel)]="email" />
|
|
51
|
+
</kt-field>
|
|
52
|
+
``` */
|
|
53
|
+
class KtField {
|
|
54
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
55
|
+
/** Libellé du champ. @default undefined */
|
|
56
|
+
label = input(/* @ts-ignore */
|
|
57
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** Texte d'aide affiché sous le contrôle. @default undefined */
|
|
59
|
+
hint = input(/* @ts-ignore */
|
|
60
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
61
|
+
/** Contenu de l'aide contextuelle (texte ou `TemplateRef`) rendu dans le tooltip d'aide. @default undefined */
|
|
62
|
+
helpText = input(/* @ts-ignore */
|
|
63
|
+
...(ngDevMode ? [undefined, { debugName: "helpText" }] : /* istanbul ignore next */ []));
|
|
64
|
+
/** Libellé accessible du déclencheur d'aide. @default KT_FIELD_CONFIG.helpLabel ?? 'Help' */
|
|
65
|
+
helpLabel = input(this.config?.helpLabel ?? 'Help', /* @ts-ignore */
|
|
66
|
+
...(ngDevMode ? [{ debugName: "helpLabel" }] : /* istanbul ignore next */ []));
|
|
67
|
+
/** id(s) externe(s) à ajouter à l'`aria-describedby` du contrôle (découplage). @default undefined */
|
|
68
|
+
customDescribedBy = input(/* @ts-ignore */
|
|
69
|
+
...(ngDevMode ? [undefined, { debugName: "customDescribedBy" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/** Émis au clic sur le déclencheur d'aide (le `preventDefault`/`stopPropagation` est déjà appliqué). */
|
|
71
|
+
helpClick = output();
|
|
72
|
+
/** Erreurs de validation à afficher sous le contrôle. @default [] */
|
|
73
|
+
errors = input([], /* @ts-ignore */
|
|
74
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
75
|
+
/** Marque le champ comme invalide (pose `aria-invalid` et conditionne l'affichage des erreurs). @default false */
|
|
76
|
+
invalid = input(false, /* @ts-ignore */
|
|
77
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
78
|
+
/** Affiche l'astérisque requis et pose `aria-required` sur le contrôle. @default false */
|
|
79
|
+
required = input(false, /* @ts-ignore */
|
|
80
|
+
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
81
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. */
|
|
82
|
+
fieldId = input(/* @ts-ignore */
|
|
83
|
+
...(ngDevMode ? [undefined, { debugName: "fieldId" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/** Masquer le hint quand l'erreur s'affiche (façon Material). */
|
|
85
|
+
hideHintWhenInvalid = input(this.config?.hideHintWhenInvalid ?? false, /* @ts-ignore */
|
|
86
|
+
...(ngDevMode ? [{ debugName: "hideHintWhenInvalid" }] : /* istanbul ignore next */ []));
|
|
87
|
+
/** Afficher toutes les erreurs au lieu de la première seule. */
|
|
88
|
+
showAllErrors = input(this.config?.showAllErrors ?? false, /* @ts-ignore */
|
|
89
|
+
...(ngDevMode ? [{ debugName: "showAllErrors" }] : /* istanbul ignore next */ []));
|
|
90
|
+
// Le contrôle est projeté : l'injection DI ne le traverse pas, on le cible via contentChild.
|
|
91
|
+
control = contentChild(KtFieldControl, /* @ts-ignore */
|
|
92
|
+
...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
|
|
93
|
+
// Détecte le tooltip d'aide standard défini dans field.html (input DX)
|
|
94
|
+
templateTooltip = viewChild(KtTooltip, /* @ts-ignore */
|
|
95
|
+
...(ngDevMode ? [{ debugName: "templateTooltip" }] : /* istanbul ignore next */ []));
|
|
96
|
+
// Détecte un tooltip d'aide custom projeté dans le slot ktFieldHelp
|
|
97
|
+
projectedTooltip = contentChild(KtTooltip, { ...(ngDevMode ? { debugName: "projectedTooltip" } : /* istanbul ignore next */ {}), descendants: true });
|
|
98
|
+
// Résout le tooltip actif
|
|
99
|
+
activeTooltip = computed(() => this.projectedTooltip() ?? this.templateTooltip(), /* @ts-ignore */
|
|
100
|
+
...(ngDevMode ? [{ debugName: "activeTooltip" }] : /* istanbul ignore next */ []));
|
|
101
|
+
idGen = inject(KtIdGenerator);
|
|
102
|
+
uid = this.idGen.generateId();
|
|
103
|
+
baseId = computed(() => this.fieldId() ?? `kt-field-${this.uid}`, /* @ts-ignore */
|
|
104
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
105
|
+
labelId = computed(() => `${this.baseId()}-label`, /* @ts-ignore */
|
|
106
|
+
...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
107
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
108
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
109
|
+
errorId = computed(() => `${this.baseId()}-error`, /* @ts-ignore */
|
|
110
|
+
...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
111
|
+
hasError = computed(() => this.invalid() && this.errors().length > 0, /* @ts-ignore */
|
|
112
|
+
...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
|
|
113
|
+
// Hint visible sauf s'il est masqué en erreur (option façon Material).
|
|
114
|
+
showHint = computed(() => !!this.hint() && !(this.hideHintWhenInvalid() && this.invalid()), /* @ts-ignore */
|
|
115
|
+
...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
|
|
116
|
+
// Première erreur seule par défaut ; toutes si showAllErrors.
|
|
117
|
+
displayedErrors = computed(() => this.showAllErrors() ? this.errors() : this.errors().slice(0, 1), /* @ts-ignore */
|
|
118
|
+
...(ngDevMode ? [{ debugName: "displayedErrors" }] : /* istanbul ignore next */ []));
|
|
119
|
+
describedBy = computed(() => {
|
|
120
|
+
const ids = [];
|
|
121
|
+
if (this.showHint())
|
|
122
|
+
ids.push(this.hintId());
|
|
123
|
+
if (this.hasError())
|
|
124
|
+
ids.push(this.errorId());
|
|
125
|
+
// Liaison a11y automatique avec le Tooltip d'aide actif (si présent et actif)
|
|
126
|
+
const tooltip = this.activeTooltip();
|
|
127
|
+
if (tooltip && tooltip.isActive()) {
|
|
128
|
+
ids.push(tooltip.idForA11y);
|
|
129
|
+
}
|
|
130
|
+
// Ajout de l'ID personnalisé externe (decouplage complet)
|
|
131
|
+
const custom = this.customDescribedBy();
|
|
132
|
+
if (custom)
|
|
133
|
+
ids.push(custom);
|
|
134
|
+
return ids.length ? ids.join(' ') : null;
|
|
135
|
+
}, /* @ts-ignore */
|
|
136
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
137
|
+
onHelpClick(event) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
event.stopPropagation();
|
|
140
|
+
this.helpClick.emit(event);
|
|
141
|
+
}
|
|
142
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtField, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
143
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtField, isStandalone: true, selector: "kt-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, helpText: { classPropertyName: "helpText", publicName: "helpText", isSignal: true, isRequired: false, transformFunction: null }, helpLabel: { classPropertyName: "helpLabel", publicName: "helpLabel", isSignal: true, isRequired: false, transformFunction: null }, customDescribedBy: { classPropertyName: "customDescribedBy", publicName: "customDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, fieldId: { classPropertyName: "fieldId", publicName: "fieldId", isSignal: true, isRequired: false, transformFunction: null }, hideHintWhenInvalid: { classPropertyName: "hideHintWhenInvalid", publicName: "hideHintWhenInvalid", isSignal: true, isRequired: false, transformFunction: null }, showAllErrors: { classPropertyName: "showAllErrors", publicName: "showAllErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { helpClick: "helpClick" }, providers: [{ provide: KT_FIELD, useExisting: forwardRef(() => KtField) }], queries: [{ propertyName: "control", first: true, predicate: KtFieldControl, descendants: true, isSignal: true }, { propertyName: "projectedTooltip", first: true, predicate: KtTooltip, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "templateTooltip", first: true, predicate: KtTooltip, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"kt-field__header\">\n @if (label(); as labelText) {\n <label class=\"kt-field__label\" [id]=\"labelId()\" [for]=\"baseId()\">\n {{ labelText }}\n @if (required()) {\n <span class=\"kt-field__required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n @if (helpText(); as help) {\n <button\n type=\"button\"\n class=\"kt-field__help\"\n [ktTooltip]=\"help\"\n [attr.aria-label]=\"helpLabel()\"\n [attr.aria-describedby]=\"null\"\n (click)=\"onHelpClick($event)\"\n >\n <span class=\"kt-field__help-icon\" aria-hidden=\"true\">help</span>\n </button>\n }\n\n <ng-content select=\"[ktFieldHelp]\" />\n</div>\n\n<div class=\"kt-field__control\">\n <ng-content />\n</div>\n\n@if (showHint()) {\n <p class=\"kt-field__hint\" [id]=\"hintId()\">{{ hint() }}</p>\n}\n\n<!-- R\u00E9gion live toujours pr\u00E9sente : les AT annoncent l'erreur d\u00E8s son apparition. -->\n<p class=\"kt-field__error\" [id]=\"errorId()\" aria-live=\"polite\">\n @if (hasError()) {\n @for (error of displayedErrors(); track $index) {\n <span class=\"kt-field__error-message\">{{ error.message }}</span>\n }\n }\n</p>\n", styles: ["@layer kt-aaa.components{:host{display:flex;flex-direction:column;gap:var(--field-gap, .375rem);font-size:var(--field-font-size, 1rem);color:var(--field-color, inherit)}.kt-field__header{display:flex;align-items:center;gap:.375rem}.kt-field__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);text-transform:var(--field-label-transform, none);letter-spacing:var(--field-label-letter-spacing, normal);color:var(--field-label-color, inherit)}.kt-field__help{display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:var(--field-icon-color, #474747);cursor:pointer;position:relative}.kt-field__help:after{content:\"\";position:absolute;inset:-12px}.kt-field__help-icon{font-family:Material Symbols Outlined;font-size:1.25rem;line-height:1;font-feature-settings:\"liga\";-webkit-font-smoothing:antialiased}.kt-field__required{margin-inline-start:.125rem;color:var(--field-required-color, #8c1d18)}.kt-field__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-field__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-field__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-field__error-message{animation:none}}}\n"], dependencies: [{ kind: "directive", type: KtTooltip, selector: "[ktTooltip]", inputs: ["ktTooltip", "tooltipPosition", "tooltipDisabled", "showDelay", "hideDelay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
144
|
+
}
|
|
145
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtField, decorators: [{
|
|
146
|
+
type: Component,
|
|
147
|
+
args: [{ selector: 'kt-field', imports: [KtTooltip], changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: KT_FIELD, useExisting: forwardRef(() => KtField) }], template: "<div class=\"kt-field__header\">\n @if (label(); as labelText) {\n <label class=\"kt-field__label\" [id]=\"labelId()\" [for]=\"baseId()\">\n {{ labelText }}\n @if (required()) {\n <span class=\"kt-field__required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n @if (helpText(); as help) {\n <button\n type=\"button\"\n class=\"kt-field__help\"\n [ktTooltip]=\"help\"\n [attr.aria-label]=\"helpLabel()\"\n [attr.aria-describedby]=\"null\"\n (click)=\"onHelpClick($event)\"\n >\n <span class=\"kt-field__help-icon\" aria-hidden=\"true\">help</span>\n </button>\n }\n\n <ng-content select=\"[ktFieldHelp]\" />\n</div>\n\n<div class=\"kt-field__control\">\n <ng-content />\n</div>\n\n@if (showHint()) {\n <p class=\"kt-field__hint\" [id]=\"hintId()\">{{ hint() }}</p>\n}\n\n<!-- R\u00E9gion live toujours pr\u00E9sente : les AT annoncent l'erreur d\u00E8s son apparition. -->\n<p class=\"kt-field__error\" [id]=\"errorId()\" aria-live=\"polite\">\n @if (hasError()) {\n @for (error of displayedErrors(); track $index) {\n <span class=\"kt-field__error-message\">{{ error.message }}</span>\n }\n }\n</p>\n", styles: ["@layer kt-aaa.components{:host{display:flex;flex-direction:column;gap:var(--field-gap, .375rem);font-size:var(--field-font-size, 1rem);color:var(--field-color, inherit)}.kt-field__header{display:flex;align-items:center;gap:.375rem}.kt-field__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);text-transform:var(--field-label-transform, none);letter-spacing:var(--field-label-letter-spacing, normal);color:var(--field-label-color, inherit)}.kt-field__help{display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:var(--field-icon-color, #474747);cursor:pointer;position:relative}.kt-field__help:after{content:\"\";position:absolute;inset:-12px}.kt-field__help-icon{font-family:Material Symbols Outlined;font-size:1.25rem;line-height:1;font-feature-settings:\"liga\";-webkit-font-smoothing:antialiased}.kt-field__required{margin-inline-start:.125rem;color:var(--field-required-color, #8c1d18)}.kt-field__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-field__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-field__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-field__error-message{animation:none}}}\n"] }]
|
|
148
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], helpText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpText", required: false }] }], helpLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpLabel", required: false }] }], customDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "customDescribedBy", required: false }] }], helpClick: [{ type: i0.Output, args: ["helpClick"] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], fieldId: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldId", required: false }] }], hideHintWhenInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHintWhenInvalid", required: false }] }], showAllErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAllErrors", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtFieldControl), { isSignal: true }] }], templateTooltip: [{ type: i0.ViewChild, args: [i0.forwardRef(() => KtTooltip), { isSignal: true }] }], projectedTooltip: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtTooltip), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
149
|
+
|
|
150
|
+
/** Base partagée des champs simples (TextField, NumberField) : état FormValueControl,
|
|
151
|
+
présentation (label/hint/prefix/suffix/clear) et politique d'affichage des erreurs.
|
|
152
|
+
La valeur, son parsing et la notion de « vide » sont fournis par la sous-classe. */
|
|
153
|
+
class KtBaseInputField {
|
|
154
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
155
|
+
// --- État poussé par [formField] ---
|
|
156
|
+
// Ces entrées sont généralement câblées par l'intégration Signal Forms (`[formField]`) ;
|
|
157
|
+
// tu peux aussi les piloter à la main en usage contrôlé.
|
|
158
|
+
/** Marque le champ comme « visité » (déclenche l'affichage des erreurs). Deux-way. @default false */
|
|
159
|
+
touched = model(false, /* @ts-ignore */
|
|
160
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
161
|
+
/** Désactive la saisie et le focus. @default false */
|
|
162
|
+
disabled = input(false, /* @ts-ignore */
|
|
163
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
164
|
+
/** Rend le champ en lecture seule (valeur visible, non modifiable). @default false */
|
|
165
|
+
readonly = input(false, /* @ts-ignore */
|
|
166
|
+
...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
167
|
+
/** Le champ est-il en erreur (état fourni par la validation). @default false */
|
|
168
|
+
invalid = input(false, /* @ts-ignore */
|
|
169
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
170
|
+
/** Marque le champ comme requis (ajoute `aria-required`). @default false */
|
|
171
|
+
required = input(false, /* @ts-ignore */
|
|
172
|
+
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
173
|
+
/** Le champ a-t-il été modifié depuis sa valeur initiale. @default false */
|
|
174
|
+
dirty = input(false, /* @ts-ignore */
|
|
175
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
176
|
+
/** Liste des erreurs de validation à afficher. @default [] */
|
|
177
|
+
errors = input([], /* @ts-ignore */
|
|
178
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
179
|
+
/** Attribut `name` natif du contrôle. @default '' */
|
|
180
|
+
name = input('', /* @ts-ignore */
|
|
181
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
182
|
+
// --- Présentation ---
|
|
183
|
+
/** id imposé pour des sélecteurs de test stables ; sinon auto-généré par Field. */
|
|
184
|
+
id = input(/* @ts-ignore */
|
|
185
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
186
|
+
/** Libellé du champ (associé via `<label for>`). */
|
|
187
|
+
label = input(/* @ts-ignore */
|
|
188
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
189
|
+
/** Texte d'aide affiché sous le champ quand il est valide. */
|
|
190
|
+
hint = input(/* @ts-ignore */
|
|
191
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
192
|
+
/** Aide contextuelle riche : texte ou `TemplateRef` projeté dans une infobulle d'aide. */
|
|
193
|
+
helpText = input(/* @ts-ignore */
|
|
194
|
+
...(ngDevMode ? [undefined, { debugName: "helpText" }] : /* istanbul ignore next */ []));
|
|
195
|
+
/** Libellé accessible du bouton d'aide. @default 'Help' (ou KT_FIELD_CONFIG.helpLabel) */
|
|
196
|
+
helpLabel = input(this.config?.helpLabel ?? 'Help', /* @ts-ignore */
|
|
197
|
+
...(ngDevMode ? [{ debugName: "helpLabel" }] : /* istanbul ignore next */ []));
|
|
198
|
+
/** Force la valeur de `aria-describedby` (sinon dérivée de hint/erreur). */
|
|
199
|
+
customDescribedBy = input(/* @ts-ignore */
|
|
200
|
+
...(ngDevMode ? [undefined, { debugName: "customDescribedBy" }] : /* istanbul ignore next */ []));
|
|
201
|
+
/** Émis au clic sur le bouton d'aide contextuelle. */
|
|
202
|
+
helpClick = output();
|
|
203
|
+
/** Texte indicatif affiché dans le champ vide. */
|
|
204
|
+
placeholder = input(/* @ts-ignore */
|
|
205
|
+
...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
206
|
+
/** Indice d'autoremplissage du navigateur (attribut `autocomplete` natif) ; ex. `'current-password'`,
|
|
207
|
+
`'new-password'`, `'email'`, `'username'`, `'off'`.
|
|
208
|
+
@default undefined */
|
|
209
|
+
autocomplete = input(/* @ts-ignore */
|
|
210
|
+
...(ngDevMode ? [undefined, { debugName: "autocomplete" }] : /* istanbul ignore next */ []));
|
|
211
|
+
/** Nom d'icône Material Symbols affichée en tête de champ. */
|
|
212
|
+
icon = input(/* @ts-ignore */
|
|
213
|
+
...(ngDevMode ? [undefined, { debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
214
|
+
/** Affiche un bouton « effacer » quand le champ contient une valeur. @default false */
|
|
215
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
216
|
+
/** Contenu décoratif en tête de champ : texte ou `TemplateRef`. */
|
|
217
|
+
prefix = input(/* @ts-ignore */
|
|
218
|
+
...(ngDevMode ? [undefined, { debugName: "prefix" }] : /* istanbul ignore next */ []));
|
|
219
|
+
/** Contenu décoratif en fin de champ : texte ou `TemplateRef`. */
|
|
220
|
+
suffix = input(/* @ts-ignore */
|
|
221
|
+
...(ngDevMode ? [undefined, { debugName: "suffix" }] : /* istanbul ignore next */ []));
|
|
222
|
+
/** Libellé i18n du bouton « effacer ». @default 'Clear' (ou KT_FIELD_CONFIG.clearLabel) */
|
|
223
|
+
clearLabel = input(this.config?.clearLabel ?? 'Clear', /* @ts-ignore */
|
|
224
|
+
...(ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []));
|
|
225
|
+
/** Quand afficher l'erreur ; surcharge KT_FIELD_CONFIG et le défaut (`invalid && touched`). */
|
|
226
|
+
errorMatcher = input(/* @ts-ignore */
|
|
227
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
228
|
+
inputRef = viewChild('input', /* @ts-ignore */
|
|
229
|
+
...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
230
|
+
matcher = computed(() => this.errorMatcher() ?? this.config?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
231
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
232
|
+
// L'erreur ne s'affiche qu'au déclenchement décidé par le matcher (invalid/touched/dirty).
|
|
233
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
235
|
+
showClear = computed(() => this.clearable() && !this.disabled() && !this.readonly() && !this.isEmpty(this.value()), /* @ts-ignore */
|
|
236
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
237
|
+
asTemplate(value) {
|
|
238
|
+
return value instanceof TemplateRef ? value : null;
|
|
239
|
+
}
|
|
240
|
+
asText(value) {
|
|
241
|
+
return typeof value === 'string' ? value : null;
|
|
242
|
+
}
|
|
243
|
+
onInput(event) {
|
|
244
|
+
this.value.set(this.parse(event.target.value));
|
|
245
|
+
}
|
|
246
|
+
onKeyDown(event) {
|
|
247
|
+
if (event.key === 'Escape' && this.showClear()) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.clear();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
clear() {
|
|
253
|
+
this.value.set(this.emptyValue());
|
|
254
|
+
this.touched.set(true);
|
|
255
|
+
this.inputRef()?.nativeElement.focus();
|
|
256
|
+
}
|
|
257
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseInputField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
258
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.1", type: KtBaseInputField, isStandalone: true, inputs: { touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, helpText: { classPropertyName: "helpText", publicName: "helpText", isSignal: true, isRequired: false, transformFunction: null }, helpLabel: { classPropertyName: "helpLabel", publicName: "helpLabel", isSignal: true, isRequired: false, transformFunction: null }, customDescribedBy: { classPropertyName: "customDescribedBy", publicName: "customDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", helpClick: "helpClick" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0 });
|
|
259
|
+
}
|
|
260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseInputField, decorators: [{
|
|
261
|
+
type: Directive
|
|
262
|
+
}], propDecorators: { touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], helpText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpText", required: false }] }], helpLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpLabel", required: false }] }], customDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "customDescribedBy", required: false }] }], helpClick: [{ type: i0.Output, args: ["helpClick"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }] } });
|
|
263
|
+
|
|
264
|
+
/** Normalise une liste de suggestions en options `<datalist>`. `toValue` sérialise la valeur en
|
|
265
|
+
chaîne attendue par l'input natif (identité pour le texte, `String()` pour les nombres,
|
|
266
|
+
`serialize()` pour les champs Temporal). */
|
|
267
|
+
function normalizeKtSuggestions(suggestions, toValue) {
|
|
268
|
+
if (!suggestions)
|
|
269
|
+
return [];
|
|
270
|
+
return suggestions.map((suggestion) => {
|
|
271
|
+
if (typeof suggestion === 'object' && suggestion !== null && 'value' in suggestion) {
|
|
272
|
+
return { value: toValue(suggestion.value), label: suggestion.label ?? null };
|
|
273
|
+
}
|
|
274
|
+
return { value: toValue(suggestion), label: null };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Champ numérique : valeur `number | null` (champ vide ⇒ `null`, jamais `NaN`), intégré aux
|
|
280
|
+
* Signal Forms via `FormValueControl`. Hérite de la présentation commune (label/hint/clear/
|
|
281
|
+
* préfixe/suffixe/erreurs) de `KtBaseInputField`.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```html
|
|
285
|
+
* <kt-number-field label="Quantité" [(value)]="qty" [min]="0" [step]="1" />
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
class KtNumberField extends KtBaseInputField {
|
|
289
|
+
/** Valeur saisie (two-way). `null` = champ vide. @default null */
|
|
290
|
+
value = model(null, /* @ts-ignore */
|
|
291
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
292
|
+
/** Borne minimale (attribut `min` natif). @default undefined */
|
|
293
|
+
min = input(/* @ts-ignore */
|
|
294
|
+
...(ngDevMode ? [undefined, { debugName: "min" }] : /* istanbul ignore next */ []));
|
|
295
|
+
/** Borne maximale (attribut `max` natif). @default undefined */
|
|
296
|
+
max = input(/* @ts-ignore */
|
|
297
|
+
...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
|
|
298
|
+
/** Pas d'incrément (attribut `step` natif). @default undefined */
|
|
299
|
+
step = input(/* @ts-ignore */
|
|
300
|
+
...(ngDevMode ? [undefined, { debugName: "step" }] : /* istanbul ignore next */ []));
|
|
301
|
+
/** Suggestions d'autocomplétion proposées via un `<datalist>` natif (la saisie reste libre).
|
|
302
|
+
Valeurs simples (`number[]`) ou couples `{ value, label }` pour distinguer libellé affiché et
|
|
303
|
+
valeur insérée. @default undefined */
|
|
304
|
+
suggestions = input(/* @ts-ignore */
|
|
305
|
+
...(ngDevMode ? [undefined, { debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
306
|
+
idGen = inject(KtIdGenerator);
|
|
307
|
+
uid = this.idGen.generateId();
|
|
308
|
+
datalistId = `kt-number-field-list-${this.uid}`;
|
|
309
|
+
hasSuggestions = computed(() => (this.suggestions()?.length ?? 0) > 0, /* @ts-ignore */
|
|
310
|
+
...(ngDevMode ? [{ debugName: "hasSuggestions" }] : /* istanbul ignore next */ []));
|
|
311
|
+
datalistOptions = computed(() => normalizeKtSuggestions(this.suggestions(), (value) => String(value)), /* @ts-ignore */
|
|
312
|
+
...(ngDevMode ? [{ debugName: "datalistOptions" }] : /* istanbul ignore next */ []));
|
|
313
|
+
// Utilitaire v22 standardisé pour la synchronisation réactive propre du champ numérique
|
|
314
|
+
rawValue = transformedValue(this.value, {
|
|
315
|
+
parse: (val) => {
|
|
316
|
+
const trimmed = val.trim().replace(',', '.');
|
|
317
|
+
if (trimmed === '')
|
|
318
|
+
return { value: null };
|
|
319
|
+
const parsed = Number(trimmed);
|
|
320
|
+
if (Number.isNaN(parsed)) {
|
|
321
|
+
return { error: { kind: 'parse', message: 'La valeur saisie doit être un nombre valide.' } };
|
|
322
|
+
}
|
|
323
|
+
return { value: parsed };
|
|
324
|
+
},
|
|
325
|
+
format: (val) => (val === null ? '' : String(val)),
|
|
326
|
+
});
|
|
327
|
+
parse(raw) {
|
|
328
|
+
const trimmed = raw.trim().replace(',', '.');
|
|
329
|
+
if (trimmed === '')
|
|
330
|
+
return null;
|
|
331
|
+
const parsed = Number(trimmed);
|
|
332
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
333
|
+
}
|
|
334
|
+
onKeyDown(event) {
|
|
335
|
+
if (this.disabled() || this.readonly())
|
|
336
|
+
return;
|
|
337
|
+
super.onKeyDown(event);
|
|
338
|
+
if (event.defaultPrevented)
|
|
339
|
+
return;
|
|
340
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
|
|
341
|
+
return;
|
|
342
|
+
const isUp = event.key === 'ArrowUp';
|
|
343
|
+
this.adjustValue(isUp);
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
}
|
|
346
|
+
adjustValue(isUp) {
|
|
347
|
+
const step = this.step() ?? 1;
|
|
348
|
+
const min = this.min();
|
|
349
|
+
const max = this.max();
|
|
350
|
+
let newValue;
|
|
351
|
+
if (this.value() === null) {
|
|
352
|
+
if (min !== undefined) {
|
|
353
|
+
newValue = min;
|
|
354
|
+
}
|
|
355
|
+
else if (max !== undefined) {
|
|
356
|
+
newValue = max;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
newValue = 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
const currentValue = this.value();
|
|
364
|
+
newValue = isUp ? currentValue + step : currentValue - step;
|
|
365
|
+
}
|
|
366
|
+
if (min !== undefined && newValue < min) {
|
|
367
|
+
newValue = min;
|
|
368
|
+
}
|
|
369
|
+
if (max !== undefined && newValue > max) {
|
|
370
|
+
newValue = max;
|
|
371
|
+
}
|
|
372
|
+
// Arrondi pour éviter les erreurs de virgule flottante JS
|
|
373
|
+
const stepStr = String(step);
|
|
374
|
+
const decimalIdx = stepStr.indexOf('.');
|
|
375
|
+
if (decimalIdx !== -1) {
|
|
376
|
+
const precision = stepStr.length - decimalIdx - 1;
|
|
377
|
+
newValue = Number(newValue.toFixed(precision));
|
|
378
|
+
}
|
|
379
|
+
this.value.set(newValue);
|
|
380
|
+
}
|
|
381
|
+
emptyValue() {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
isEmpty(value) {
|
|
385
|
+
return value === null;
|
|
386
|
+
}
|
|
387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtNumberField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
388
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtNumberField, isStandalone: true, selector: "kt-number-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"text\"\n inputmode=\"decimal\"\n role=\"spinbutton\"\n [attr.aria-valuemin]=\"min() ?? null\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [value]=\"rawValue()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.step]=\"step() ?? null\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"rawValue.set($event.target.value)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
389
|
+
}
|
|
390
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtNumberField, decorators: [{
|
|
391
|
+
type: Component,
|
|
392
|
+
args: [{ selector: 'kt-number-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"text\"\n inputmode=\"decimal\"\n role=\"spinbutton\"\n [attr.aria-valuemin]=\"min() ?? null\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [value]=\"rawValue()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.step]=\"step() ?? null\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"rawValue.set($event.target.value)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
393
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }] } });
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Zone de texte multi-ligne (valeur `string`) intégrée aux Signal Forms via `FormValueControl`.
|
|
397
|
+
* Autosize en CSS (`field-sizing: content`) ; hérite de la présentation commune de `KtBaseInputField`.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```html
|
|
401
|
+
* <kt-text-area label="Commentaire" [(value)]="comment" [rows]="4" />
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
class KtTextArea extends KtBaseInputField {
|
|
405
|
+
/** Valeur saisie (two-way). @default '' */
|
|
406
|
+
value = model('', /* @ts-ignore */
|
|
407
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
408
|
+
/** Hauteur initiale (lignes). L'autosize est géré en CSS via `field-sizing: content`. @default 3 */
|
|
409
|
+
rows = input(3, /* @ts-ignore */
|
|
410
|
+
...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
411
|
+
/** Plafond de caractères ; poussé par le form (validateur maxLength) ou par le consommateur. @default undefined */
|
|
412
|
+
maxLength = input(/* @ts-ignore */
|
|
413
|
+
...(ngDevMode ? [undefined, { debugName: "maxLength" }] : /* istanbul ignore next */ []));
|
|
414
|
+
parse(raw) {
|
|
415
|
+
return raw;
|
|
416
|
+
}
|
|
417
|
+
emptyValue() {
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
isEmpty(value) {
|
|
421
|
+
return value.length === 0;
|
|
422
|
+
}
|
|
423
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTextArea, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
424
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtTextArea, isStandalone: true, selector: "kt-text-area", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n data-multiline\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <textarea\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n [value]=\"value()\"\n [rows]=\"rows()\"\n [style.min-height.lh]=\"rows()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.maxlength]=\"maxLength() ?? null\"\n (input)=\"onInput($event)\"\n (blur)=\"touched.set(true)\"\n ></textarea>\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
425
|
+
}
|
|
426
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTextArea, decorators: [{
|
|
427
|
+
type: Component,
|
|
428
|
+
args: [{ selector: 'kt-text-area', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n data-multiline\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <textarea\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n [value]=\"value()\"\n [rows]=\"rows()\"\n [style.min-height.lh]=\"rows()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.maxlength]=\"maxLength() ?? null\"\n (input)=\"onInput($event)\"\n (blur)=\"touched.set(true)\"\n ></textarea>\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
429
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }] } });
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Champ texte (valeur `string`) intégré aux Signal Forms via `FormValueControl`. Hérite de la
|
|
433
|
+
* présentation commune (label/hint/clear/préfixe/suffixe/erreurs) de `KtBaseInputField`.
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```html
|
|
437
|
+
* <kt-text-field label="E-mail" type="email" [(value)]="email" required />
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
class KtTextField extends KtBaseInputField {
|
|
441
|
+
/** Valeur saisie (two-way). @default '' */
|
|
442
|
+
value = model('', /* @ts-ignore */
|
|
443
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
444
|
+
/** Variante HTML du champ (pilote `type` natif et le clavier mobile). @default 'text' */
|
|
445
|
+
type = input('text', /* @ts-ignore */
|
|
446
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
447
|
+
/** Suggestions d'autocomplétion proposées via un `<datalist>` natif (la saisie reste libre).
|
|
448
|
+
Valeurs simples (`string[]`) ou couples `{ value, label }` pour distinguer libellé affiché et
|
|
449
|
+
valeur insérée. @default undefined */
|
|
450
|
+
suggestions = input(/* @ts-ignore */
|
|
451
|
+
...(ngDevMode ? [undefined, { debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
452
|
+
idGen = inject(KtIdGenerator);
|
|
453
|
+
uid = this.idGen.generateId();
|
|
454
|
+
datalistId = `kt-text-field-list-${this.uid}`;
|
|
455
|
+
hasSuggestions = computed(() => (this.suggestions()?.length ?? 0) > 0, /* @ts-ignore */
|
|
456
|
+
...(ngDevMode ? [{ debugName: "hasSuggestions" }] : /* istanbul ignore next */ []));
|
|
457
|
+
datalistOptions = computed(() => normalizeKtSuggestions(this.suggestions(), (value) => value), /* @ts-ignore */
|
|
458
|
+
...(ngDevMode ? [{ debugName: "datalistOptions" }] : /* istanbul ignore next */ []));
|
|
459
|
+
parse(raw) {
|
|
460
|
+
return raw;
|
|
461
|
+
}
|
|
462
|
+
emptyValue() {
|
|
463
|
+
return '';
|
|
464
|
+
}
|
|
465
|
+
isEmpty(value) {
|
|
466
|
+
return value.length === 0;
|
|
467
|
+
}
|
|
468
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTextField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
469
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtTextField, isStandalone: true, selector: "kt-text-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n [type]=\"type()\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
470
|
+
}
|
|
471
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTextField, decorators: [{
|
|
472
|
+
type: Component,
|
|
473
|
+
args: [{ selector: 'kt-text-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n [type]=\"type()\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
474
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }] } });
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Bouton bascule (Switch / Slide-Toggle) accessible conforme aux normes WAI-ARIA.
|
|
478
|
+
* Permet d'activer ou désactiver une option avec effet immédiat.
|
|
479
|
+
* Intégré aux Signal Forms de l'application via FormValueControl.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```html
|
|
483
|
+
* <kt-switch label="Notifications par e-mail" [(value)]="emailNotif" />
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
class KtSwitch {
|
|
487
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
488
|
+
/** État de la bascule (two-way) : `true` = activé, `false` = désactivé. @default false */
|
|
489
|
+
value = model(false, /* @ts-ignore */
|
|
490
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
491
|
+
// --- État poussé par [formField] ---
|
|
492
|
+
/** État « touché » (two-way), généralement piloté par `[formField]`. @default false */
|
|
493
|
+
touched = model(false, /* @ts-ignore */
|
|
494
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
495
|
+
/** Désactive la bascule (non actionnable). @default false */
|
|
496
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
497
|
+
/** Marque la bascule comme invalide (combiné à `touched`/`dirty` via l'`errorMatcher`). @default false */
|
|
498
|
+
invalid = input(false, /* @ts-ignore */
|
|
499
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
500
|
+
/** Affiche l'astérisque requis sur le libellé. @default false */
|
|
501
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
502
|
+
/** État « modifié » (entre dans la logique d'affichage des erreurs). @default false */
|
|
503
|
+
dirty = input(false, /* @ts-ignore */
|
|
504
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
505
|
+
/** Erreurs de validation à afficher sous la bascule. @default [] */
|
|
506
|
+
errors = input([], /* @ts-ignore */
|
|
507
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
508
|
+
/** Identifiant logique du contrôle (à titre indicatif). @default '' */
|
|
509
|
+
name = input('', /* @ts-ignore */
|
|
510
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
511
|
+
// --- Présentation ---
|
|
512
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. @default undefined */
|
|
513
|
+
id = input(/* @ts-ignore */
|
|
514
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
515
|
+
/** Texte du libellé associé à la bascule. @default undefined */
|
|
516
|
+
label = input(/* @ts-ignore */
|
|
517
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
518
|
+
/** Texte d'aide affiché sous la bascule (masqué quand une erreur s'affiche). @default undefined */
|
|
519
|
+
hint = input(/* @ts-ignore */
|
|
520
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
521
|
+
/** Nom accessible (`aria-label`) quand `label` est absent. @default undefined */
|
|
522
|
+
ariaLabel = input(/* @ts-ignore */
|
|
523
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
524
|
+
/** Stratégie décidant quand afficher les erreurs (sinon celle du `KT_FIELD_CONFIG`). @default undefined */
|
|
525
|
+
errorMatcher = input(/* @ts-ignore */
|
|
526
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
527
|
+
/** Afficher toutes les erreurs au lieu de la première seule. @default KT_FIELD_CONFIG.showAllErrors ?? false */
|
|
528
|
+
showAllErrors = input(this.config?.showAllErrors ?? false, /* @ts-ignore */
|
|
529
|
+
...(ngDevMode ? [{ debugName: "showAllErrors" }] : /* istanbul ignore next */ []));
|
|
530
|
+
idGen = inject(KtIdGenerator);
|
|
531
|
+
uid = this.idGen.generateId('switch');
|
|
532
|
+
baseId = computed(() => this.id() ?? `kt-switch-${this.uid}`, /* @ts-ignore */
|
|
533
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
534
|
+
labelId = computed(() => `${this.baseId()}-label`, /* @ts-ignore */
|
|
535
|
+
...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
536
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
537
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
538
|
+
errorId = computed(() => `${this.baseId()}-error`, /* @ts-ignore */
|
|
539
|
+
...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
540
|
+
matcher = computed(() => this.errorMatcher() ?? this.config?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
541
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
542
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
543
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
544
|
+
displayedErrors = computed(() => this.showAllErrors() ? this.errors() : this.errors().slice(0, 1), /* @ts-ignore */
|
|
545
|
+
...(ngDevMode ? [{ debugName: "displayedErrors" }] : /* istanbul ignore next */ []));
|
|
546
|
+
resolvedAriaLabel = computed(() => this.ariaLabel() ?? null, /* @ts-ignore */
|
|
547
|
+
...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
548
|
+
describedBy = computed(() => {
|
|
549
|
+
const ids = [];
|
|
550
|
+
if (this.hint() && !this.showInvalid())
|
|
551
|
+
ids.push(this.hintId());
|
|
552
|
+
if (this.showInvalid() && this.errors().length > 0)
|
|
553
|
+
ids.push(this.errorId());
|
|
554
|
+
return ids.length ? ids.join(' ') : null;
|
|
555
|
+
}, /* @ts-ignore */
|
|
556
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
557
|
+
toggle() {
|
|
558
|
+
if (this.disabled())
|
|
559
|
+
return;
|
|
560
|
+
this.value.update((v) => !v);
|
|
561
|
+
this.touched.set(true);
|
|
562
|
+
}
|
|
563
|
+
onSpacebar(event) {
|
|
564
|
+
event.preventDefault(); // évite le scroll de la page au clavier
|
|
565
|
+
this.toggle();
|
|
566
|
+
}
|
|
567
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSwitch, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
568
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtSwitch, isStandalone: true, selector: "kt-switch", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null }, showAllErrors: { classPropertyName: "showAllErrors", publicName: "showAllErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: `
|
|
569
|
+
<div
|
|
570
|
+
class="kt-switch-field"
|
|
571
|
+
[class.kt-switch-field--invalid]="showInvalid()"
|
|
572
|
+
[class.kt-switch-field--disabled]="disabled()"
|
|
573
|
+
>
|
|
574
|
+
<div class="kt-switch-row">
|
|
575
|
+
<button
|
|
576
|
+
#switchBtn
|
|
577
|
+
type="button"
|
|
578
|
+
role="switch"
|
|
579
|
+
[id]="baseId()"
|
|
580
|
+
[attr.aria-checked]="value()"
|
|
581
|
+
[attr.aria-label]="resolvedAriaLabel()"
|
|
582
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
583
|
+
[attr.aria-describedby]="describedBy()"
|
|
584
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
585
|
+
[disabled]="disabled()"
|
|
586
|
+
(click)="toggle()"
|
|
587
|
+
(keydown.space)="onSpacebar($event)"
|
|
588
|
+
class="kt-switch"
|
|
589
|
+
>
|
|
590
|
+
<span class="kt-switch__thumb"></span>
|
|
591
|
+
</button>
|
|
592
|
+
|
|
593
|
+
@if (label(); as labelText) {
|
|
594
|
+
<label [id]="labelId()" [attr.for]="baseId()" class="kt-switch-label">
|
|
595
|
+
{{ labelText }}
|
|
596
|
+
@if (required()) {
|
|
597
|
+
<span class="kt-switch-label__required" aria-hidden="true">*</span>
|
|
598
|
+
}
|
|
599
|
+
</label>
|
|
600
|
+
}
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
@if (hint() && !showInvalid()) {
|
|
604
|
+
<p [id]="hintId()" class="kt-switch-hint">{{ hint() }}</p>
|
|
605
|
+
}
|
|
606
|
+
<div [id]="errorId()" class="kt-switch-error" aria-live="polite">
|
|
607
|
+
@if (showInvalid()) {
|
|
608
|
+
@for (error of displayedErrors(); track $index) {
|
|
609
|
+
<span class="kt-switch-error-message">{{ error.message }}</span>
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
`, isInline: true, styles: ["@layer kt-aaa.components{:host{display:block}.kt-switch-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-switch-row{display:flex;align-items:center;gap:var(--field-control-gap, .5rem)}.kt-switch{position:relative;display:inline-flex;align-items:center;box-sizing:border-box;inline-size:var(--switch-width, 2.75rem);block-size:var(--switch-height, 1.5rem);padding:var(--switch-padding, 2px);border-radius:var(--switch-radius, 999px);border:var(--switch-border-width, 2px) solid var(--switch-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));background-color:var(--switch-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;outline:none;box-shadow:var(--switch-shadow, var(--field-shadow, none));transition:var( --switch-transition, background-color .2s cubic-bezier(.4, 0, .2, 1), border-color .2s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-switch:after{content:\"\";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:100%;height:44px}.kt-switch__thumb{display:block;inline-size:var(--switch-thumb-size, 1rem);block-size:var(--switch-thumb-size, 1rem);border-radius:var(--switch-thumb-radius, 50%);background-color:var(--switch-thumb-bg, var(--kt-outline-strong, #5f6368));box-shadow:var(--switch-thumb-shadow, 0 1px 3px rgba(0, 0, 0, .2));transition:var( --switch-thumb-transition, transform .2s cubic-bezier(.34, 1.56, .64, 1), background-color .2s ease );transform:translate(0)}.kt-switch[aria-checked=true]{background-color:var(--switch-bg-active, var(--kt-primary, #0b57d0));border-color:var(--switch-border-color-active, var(--kt-primary, #0b57d0));box-shadow:var( --switch-shadow-active, 0 0 8px color-mix(in srgb, var(--switch-bg-active, var(--kt-primary, #0b57d0)) 50%, transparent) )}.kt-switch[aria-checked=true] .kt-switch__thumb{background-color:var(--switch-thumb-bg-active, #ffffff);transform:translate(calc(var(--switch-width, 2.75rem) - var(--switch-thumb-size, 1rem) - (var(--switch-padding, 2px) * 2) - (var(--switch-border-width, 2px) * 2)))}.kt-switch:hover:not(:disabled){border-color:var(--switch-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)));box-shadow:var(--switch-shadow-hover, var(--field-shadow-hover, var(--switch-shadow, var(--field-shadow, none))))}.kt-switch[aria-checked=true]:hover:not(:disabled){background-color:var(--switch-bg-active-hover, color-mix(in srgb, var(--kt-primary, #0b57d0) 90%, black));border-color:var(--switch-border-color-active-hover, color-mix(in srgb, var(--kt-primary, #0b57d0) 90%, black))}.kt-switch:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0b57d0);outline-offset:var(--switch-focus-ring-offset, 2px);box-shadow:var(--switch-shadow-focus, var(--field-shadow-focus, var(--switch-shadow, var(--field-shadow, none))))}.kt-switch:disabled{cursor:not-allowed;opacity:.5;background-color:var(--field-disabled-bg, #f1f3f4);border-color:var(--field-border-color, #c4c7c5)}.kt-switch:disabled .kt-switch__thumb{background-color:var(--kt-outline, #c4c7c5);box-shadow:none}.kt-switch-field--invalid .kt-switch{border-color:var(--field-error-color, var(--kt-danger, #b3261e))}.kt-switch-field--invalid .kt-switch[aria-checked=true]{background-color:var(--field-error-color, var(--kt-danger, #b3261e));border-color:var(--field-error-color, var(--kt-danger, #b3261e))}.kt-switch-label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit);cursor:pointer;-webkit-user-select:none;user-select:none}.kt-switch-field--disabled .kt-switch-label{cursor:not-allowed;color:var(--field-hint-color, #5f6368);opacity:.6}.kt-switch-label__required{margin-inline-start:.125rem;color:var(--field-required-color, #8c1d18)}.kt-switch-hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-switch-error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-switch-error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-switch,.kt-switch__thumb{transition:none}.kt-switch-error-message{animation:none}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
615
|
+
}
|
|
616
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSwitch, decorators: [{
|
|
617
|
+
type: Component,
|
|
618
|
+
args: [{ selector: 'kt-switch', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
619
|
+
<div
|
|
620
|
+
class="kt-switch-field"
|
|
621
|
+
[class.kt-switch-field--invalid]="showInvalid()"
|
|
622
|
+
[class.kt-switch-field--disabled]="disabled()"
|
|
623
|
+
>
|
|
624
|
+
<div class="kt-switch-row">
|
|
625
|
+
<button
|
|
626
|
+
#switchBtn
|
|
627
|
+
type="button"
|
|
628
|
+
role="switch"
|
|
629
|
+
[id]="baseId()"
|
|
630
|
+
[attr.aria-checked]="value()"
|
|
631
|
+
[attr.aria-label]="resolvedAriaLabel()"
|
|
632
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
633
|
+
[attr.aria-describedby]="describedBy()"
|
|
634
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
635
|
+
[disabled]="disabled()"
|
|
636
|
+
(click)="toggle()"
|
|
637
|
+
(keydown.space)="onSpacebar($event)"
|
|
638
|
+
class="kt-switch"
|
|
639
|
+
>
|
|
640
|
+
<span class="kt-switch__thumb"></span>
|
|
641
|
+
</button>
|
|
642
|
+
|
|
643
|
+
@if (label(); as labelText) {
|
|
644
|
+
<label [id]="labelId()" [attr.for]="baseId()" class="kt-switch-label">
|
|
645
|
+
{{ labelText }}
|
|
646
|
+
@if (required()) {
|
|
647
|
+
<span class="kt-switch-label__required" aria-hidden="true">*</span>
|
|
648
|
+
}
|
|
649
|
+
</label>
|
|
650
|
+
}
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
@if (hint() && !showInvalid()) {
|
|
654
|
+
<p [id]="hintId()" class="kt-switch-hint">{{ hint() }}</p>
|
|
655
|
+
}
|
|
656
|
+
<div [id]="errorId()" class="kt-switch-error" aria-live="polite">
|
|
657
|
+
@if (showInvalid()) {
|
|
658
|
+
@for (error of displayedErrors(); track $index) {
|
|
659
|
+
<span class="kt-switch-error-message">{{ error.message }}</span>
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
`, styles: ["@layer kt-aaa.components{:host{display:block}.kt-switch-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-switch-row{display:flex;align-items:center;gap:var(--field-control-gap, .5rem)}.kt-switch{position:relative;display:inline-flex;align-items:center;box-sizing:border-box;inline-size:var(--switch-width, 2.75rem);block-size:var(--switch-height, 1.5rem);padding:var(--switch-padding, 2px);border-radius:var(--switch-radius, 999px);border:var(--switch-border-width, 2px) solid var(--switch-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));background-color:var(--switch-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;outline:none;box-shadow:var(--switch-shadow, var(--field-shadow, none));transition:var( --switch-transition, background-color .2s cubic-bezier(.4, 0, .2, 1), border-color .2s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-switch:after{content:\"\";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:100%;height:44px}.kt-switch__thumb{display:block;inline-size:var(--switch-thumb-size, 1rem);block-size:var(--switch-thumb-size, 1rem);border-radius:var(--switch-thumb-radius, 50%);background-color:var(--switch-thumb-bg, var(--kt-outline-strong, #5f6368));box-shadow:var(--switch-thumb-shadow, 0 1px 3px rgba(0, 0, 0, .2));transition:var( --switch-thumb-transition, transform .2s cubic-bezier(.34, 1.56, .64, 1), background-color .2s ease );transform:translate(0)}.kt-switch[aria-checked=true]{background-color:var(--switch-bg-active, var(--kt-primary, #0b57d0));border-color:var(--switch-border-color-active, var(--kt-primary, #0b57d0));box-shadow:var( --switch-shadow-active, 0 0 8px color-mix(in srgb, var(--switch-bg-active, var(--kt-primary, #0b57d0)) 50%, transparent) )}.kt-switch[aria-checked=true] .kt-switch__thumb{background-color:var(--switch-thumb-bg-active, #ffffff);transform:translate(calc(var(--switch-width, 2.75rem) - var(--switch-thumb-size, 1rem) - (var(--switch-padding, 2px) * 2) - (var(--switch-border-width, 2px) * 2)))}.kt-switch:hover:not(:disabled){border-color:var(--switch-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)));box-shadow:var(--switch-shadow-hover, var(--field-shadow-hover, var(--switch-shadow, var(--field-shadow, none))))}.kt-switch[aria-checked=true]:hover:not(:disabled){background-color:var(--switch-bg-active-hover, color-mix(in srgb, var(--kt-primary, #0b57d0) 90%, black));border-color:var(--switch-border-color-active-hover, color-mix(in srgb, var(--kt-primary, #0b57d0) 90%, black))}.kt-switch:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0b57d0);outline-offset:var(--switch-focus-ring-offset, 2px);box-shadow:var(--switch-shadow-focus, var(--field-shadow-focus, var(--switch-shadow, var(--field-shadow, none))))}.kt-switch:disabled{cursor:not-allowed;opacity:.5;background-color:var(--field-disabled-bg, #f1f3f4);border-color:var(--field-border-color, #c4c7c5)}.kt-switch:disabled .kt-switch__thumb{background-color:var(--kt-outline, #c4c7c5);box-shadow:none}.kt-switch-field--invalid .kt-switch{border-color:var(--field-error-color, var(--kt-danger, #b3261e))}.kt-switch-field--invalid .kt-switch[aria-checked=true]{background-color:var(--field-error-color, var(--kt-danger, #b3261e));border-color:var(--field-error-color, var(--kt-danger, #b3261e))}.kt-switch-label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit);cursor:pointer;-webkit-user-select:none;user-select:none}.kt-switch-field--disabled .kt-switch-label{cursor:not-allowed;color:var(--field-hint-color, #5f6368);opacity:.6}.kt-switch-label__required{margin-inline-start:.125rem;color:var(--field-required-color, #8c1d18)}.kt-switch-hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-switch-error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-switch-error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-switch,.kt-switch__thumb{transition:none}.kt-switch-error-message{animation:none}}}\n"] }]
|
|
665
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], showAllErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAllErrors", required: false }] }] } });
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Groupe de cases à cocher (Checkbox Group) conforme à l'ARIA APG / RGAA.
|
|
669
|
+
* Orchestrateur : porte une valeur TABLEAU (`FormValueControl<V[]>`) et la sémantique de groupe
|
|
670
|
+
* (`role="group"`, légende, `aria-required`/`aria-invalid`, erreurs).
|
|
671
|
+
*
|
|
672
|
+
* Différence APG avec le radio-group : PAS de roving tabindex — chaque `kt-checkbox` reste
|
|
673
|
+
* tabbable et coche/décoche indépendamment. Les options sont fournies par le dev (boucle `@for`
|
|
674
|
+
* ou en dur) ; `compareWith` gère l'égalité quand les valeurs d'option sont des objets.
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```html
|
|
678
|
+
* <kt-checkbox-group label="Centres d'intérêt" [(value)]="interests" required>
|
|
679
|
+
* <kt-checkbox [value]="'sport'" label="Sport" />
|
|
680
|
+
* <kt-checkbox [value]="'musique'" label="Musique" />
|
|
681
|
+
* <kt-checkbox [value]="'cinema'" label="Cinéma" />
|
|
682
|
+
* </kt-checkbox-group>
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
class KtCheckboxGroup {
|
|
686
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
687
|
+
/** Sélection courante : tableau des valeurs d'options cochées (two-way). @default [] */
|
|
688
|
+
value = model([], /* @ts-ignore */
|
|
689
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
690
|
+
// --- État poussé par [formField] ---
|
|
691
|
+
/** État « touché » (two-way), généralement piloté par `[formField]`. @default false */
|
|
692
|
+
touched = model(false, /* @ts-ignore */
|
|
693
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
694
|
+
/** Désactive le groupe entier (hérité par chaque case enfant). @default false */
|
|
695
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
696
|
+
/** Marque le groupe comme invalide (combiné à `touched`/`dirty` via l'`errorMatcher`). @default false */
|
|
697
|
+
invalid = input(false, /* @ts-ignore */
|
|
698
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
699
|
+
/** Affiche l'astérisque requis et `aria-required` sur le groupe. @default false */
|
|
700
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
701
|
+
/** État « modifié » (entre dans la logique d'affichage des erreurs). @default false */
|
|
702
|
+
dirty = input(false, /* @ts-ignore */
|
|
703
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
704
|
+
/** Erreurs de validation à afficher sous le groupe. @default [] */
|
|
705
|
+
errors = input([], /* @ts-ignore */
|
|
706
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
707
|
+
/** Attribut `name` partagé (à titre indicatif). @default '' */
|
|
708
|
+
name = input('', /* @ts-ignore */
|
|
709
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
710
|
+
// --- Présentation ---
|
|
711
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. @default undefined */
|
|
712
|
+
id = input(/* @ts-ignore */
|
|
713
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
714
|
+
/** Légende du groupe (`aria-labelledby`). @default undefined */
|
|
715
|
+
label = input(/* @ts-ignore */
|
|
716
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
717
|
+
/** Texte d'aide affiché sous le groupe (masqué quand une erreur s'affiche). @default undefined */
|
|
718
|
+
hint = input(/* @ts-ignore */
|
|
719
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
720
|
+
/** Nom accessible (`aria-label`) du groupe quand `label` est absent. @default undefined */
|
|
721
|
+
ariaLabel = input(/* @ts-ignore */
|
|
722
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
723
|
+
/** Stratégie décidant quand afficher les erreurs (sinon celle du `KT_FIELD_CONFIG`). @default undefined */
|
|
724
|
+
errorMatcher = input(/* @ts-ignore */
|
|
725
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
726
|
+
/** Afficher toutes les erreurs au lieu de la première seule. @default KT_FIELD_CONFIG.showAllErrors ?? false */
|
|
727
|
+
showAllErrors = input(this.config?.showAllErrors ?? false, /* @ts-ignore */
|
|
728
|
+
...(ngDevMode ? [{ debugName: "showAllErrors" }] : /* istanbul ignore next */ []));
|
|
729
|
+
/** Égalité des valeurs en mode objet (défaut : identité `===`). */
|
|
730
|
+
compareWith = input(/* @ts-ignore */
|
|
731
|
+
...(ngDevMode ? [undefined, { debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
732
|
+
idGen = inject(KtIdGenerator);
|
|
733
|
+
uid = this.idGen.generateId('checkbox-group');
|
|
734
|
+
baseId = computed(() => this.id() ?? `kt-checkbox-group-${this.uid}`, /* @ts-ignore */
|
|
735
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
736
|
+
labelId = computed(() => `${this.baseId()}-label`, /* @ts-ignore */
|
|
737
|
+
...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
738
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
739
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
740
|
+
errorId = computed(() => `${this.baseId()}-error`, /* @ts-ignore */
|
|
741
|
+
...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
742
|
+
matcher = computed(() => this.errorMatcher() ?? this.config?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
743
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
744
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
745
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
746
|
+
displayedErrors = computed(() => this.showAllErrors() ? this.errors() : this.errors().slice(0, 1), /* @ts-ignore */
|
|
747
|
+
...(ngDevMode ? [{ debugName: "displayedErrors" }] : /* istanbul ignore next */ []));
|
|
748
|
+
resolvedAriaLabel = computed(() => this.ariaLabel() ?? null, /* @ts-ignore */
|
|
749
|
+
...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
750
|
+
describedBy = computed(() => {
|
|
751
|
+
const ids = [];
|
|
752
|
+
if (this.hint() && !this.showInvalid())
|
|
753
|
+
ids.push(this.hintId());
|
|
754
|
+
if (this.showInvalid() && this.errors().length > 0)
|
|
755
|
+
ids.push(this.errorId());
|
|
756
|
+
return ids.length ? ids.join(' ') : null;
|
|
757
|
+
}, /* @ts-ignore */
|
|
758
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
759
|
+
comparator = computed(() => this.compareWith() ?? ((a, b) => a === b), /* @ts-ignore */
|
|
760
|
+
...(ngDevMode ? [{ debugName: "comparator" }] : /* istanbul ignore next */ []));
|
|
761
|
+
/** La valeur d'option d'un enfant est-elle dans la sélection ? (lu dans le `computed` enfant) */
|
|
762
|
+
isSelected(optionValue) {
|
|
763
|
+
const arr = this.value() ?? [];
|
|
764
|
+
const cmp = this.comparator();
|
|
765
|
+
return arr.some((v) => cmp(v, optionValue));
|
|
766
|
+
}
|
|
767
|
+
/** Ajoute/retire une valeur d'option de la sélection (émis par un enfant au `change` natif). */
|
|
768
|
+
toggle(optionValue, checked) {
|
|
769
|
+
if (this.disabled())
|
|
770
|
+
return;
|
|
771
|
+
const arr = this.value() ?? [];
|
|
772
|
+
const cmp = this.comparator();
|
|
773
|
+
const exists = arr.some((v) => cmp(v, optionValue));
|
|
774
|
+
if (checked && !exists) {
|
|
775
|
+
this.value.set([...arr, optionValue]);
|
|
776
|
+
}
|
|
777
|
+
else if (!checked && exists) {
|
|
778
|
+
this.value.set(arr.filter((v) => !cmp(v, optionValue)));
|
|
779
|
+
}
|
|
780
|
+
this.touched.set(true);
|
|
781
|
+
}
|
|
782
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtCheckboxGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
783
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtCheckboxGroup, isStandalone: true, selector: "kt-checkbox-group", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null }, showAllErrors: { classPropertyName: "showAllErrors", publicName: "showAllErrors", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: `
|
|
784
|
+
<div
|
|
785
|
+
class="kt-checkbox-group-field"
|
|
786
|
+
[class.kt-checkbox-group-field--invalid]="showInvalid()"
|
|
787
|
+
[class.kt-checkbox-group-field--disabled]="disabled()"
|
|
788
|
+
role="group"
|
|
789
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
790
|
+
[attr.aria-label]="!label() ? resolvedAriaLabel() : null"
|
|
791
|
+
[attr.aria-describedby]="describedBy()"
|
|
792
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
793
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
794
|
+
>
|
|
795
|
+
@if (label(); as labelText) {
|
|
796
|
+
<span [id]="labelId()" class="kt-checkbox-group__legend">
|
|
797
|
+
{{ labelText }}
|
|
798
|
+
@if (required()) {
|
|
799
|
+
<span class="kt-checkbox-group__required" aria-hidden="true">*</span>
|
|
800
|
+
}
|
|
801
|
+
</span>
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
<div class="kt-checkbox-group__options">
|
|
805
|
+
<ng-content></ng-content>
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
@if (hint() && !showInvalid()) {
|
|
809
|
+
<p [id]="hintId()" class="kt-checkbox-group__hint">{{ hint() }}</p>
|
|
810
|
+
}
|
|
811
|
+
<div [id]="errorId()" class="kt-checkbox-group__error" aria-live="polite">
|
|
812
|
+
@if (showInvalid()) {
|
|
813
|
+
@for (error of displayedErrors(); track $index) {
|
|
814
|
+
<span class="kt-checkbox-group__error-message">{{ error.message }}</span>
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
`, isInline: true, styles: ["@layer kt-aaa.components{:host{display:block}.kt-checkbox-group-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-checkbox-group__legend{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-checkbox-group__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-checkbox-group__options{display:flex;flex-direction:var(--checkbox-group-direction, column);flex-wrap:wrap;gap:var(--checkbox-group-gap, .25rem)}.kt-checkbox-group__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-checkbox-group__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-checkbox-group__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-checkbox-group__error-message{animation:none}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
820
|
+
}
|
|
821
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtCheckboxGroup, decorators: [{
|
|
822
|
+
type: Component,
|
|
823
|
+
args: [{ selector: 'kt-checkbox-group', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
824
|
+
<div
|
|
825
|
+
class="kt-checkbox-group-field"
|
|
826
|
+
[class.kt-checkbox-group-field--invalid]="showInvalid()"
|
|
827
|
+
[class.kt-checkbox-group-field--disabled]="disabled()"
|
|
828
|
+
role="group"
|
|
829
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
830
|
+
[attr.aria-label]="!label() ? resolvedAriaLabel() : null"
|
|
831
|
+
[attr.aria-describedby]="describedBy()"
|
|
832
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
833
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
834
|
+
>
|
|
835
|
+
@if (label(); as labelText) {
|
|
836
|
+
<span [id]="labelId()" class="kt-checkbox-group__legend">
|
|
837
|
+
{{ labelText }}
|
|
838
|
+
@if (required()) {
|
|
839
|
+
<span class="kt-checkbox-group__required" aria-hidden="true">*</span>
|
|
840
|
+
}
|
|
841
|
+
</span>
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
<div class="kt-checkbox-group__options">
|
|
845
|
+
<ng-content></ng-content>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
@if (hint() && !showInvalid()) {
|
|
849
|
+
<p [id]="hintId()" class="kt-checkbox-group__hint">{{ hint() }}</p>
|
|
850
|
+
}
|
|
851
|
+
<div [id]="errorId()" class="kt-checkbox-group__error" aria-live="polite">
|
|
852
|
+
@if (showInvalid()) {
|
|
853
|
+
@for (error of displayedErrors(); track $index) {
|
|
854
|
+
<span class="kt-checkbox-group__error-message">{{ error.message }}</span>
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
`, styles: ["@layer kt-aaa.components{:host{display:block}.kt-checkbox-group-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-checkbox-group__legend{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-checkbox-group__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-checkbox-group__options{display:flex;flex-direction:var(--checkbox-group-direction, column);flex-wrap:wrap;gap:var(--checkbox-group-gap, .25rem)}.kt-checkbox-group__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-checkbox-group__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-checkbox-group__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-checkbox-group__error-message{animation:none}}}\n"] }]
|
|
860
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], showAllErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAllErrors", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }] } });
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Case à cocher (Checkbox) accessible conforme aux normes WAI-ARIA / RGAA.
|
|
864
|
+
* Bâtie sur un `<input type="checkbox">` NATIF (clavier, clic-label, état `indeterminate`,
|
|
865
|
+
* mode contraste élevé gratuits), redessinée via `appearance: none` + pseudo-éléments thémés.
|
|
866
|
+
*
|
|
867
|
+
* DEUX MODES, selon la présence d'un `kt-checkbox-group` parent :
|
|
868
|
+
* - Autonome : `value` est l'état coché (`boolean`, two-way), intégré aux Signal Forms via
|
|
869
|
+
* `FormValueControl<boolean>` (même contrat que `kt-switch`).
|
|
870
|
+
* - Dans un groupe : `value` est la VALEUR d'option représentée ; l'état coché dérive de
|
|
871
|
+
* l'appartenance au tableau du groupe, et le (dé)cochage met à jour ce tableau.
|
|
872
|
+
*
|
|
873
|
+
* Libellé : `label` (texte) par défaut ; un contenu projeté le remplace visuellement pour les
|
|
874
|
+
* rendus riches (carte/option), `ariaLabel` portant alors le nom accessible si besoin.
|
|
875
|
+
*
|
|
876
|
+
* @example
|
|
877
|
+
* ```html
|
|
878
|
+
* <!-- Autonome -->
|
|
879
|
+
* <kt-checkbox label="Accepter les conditions" [(value)]="accepted" required />
|
|
880
|
+
*
|
|
881
|
+
* <!-- Dans un groupe -->
|
|
882
|
+
* <kt-checkbox-group label="Centres d'intérêt" [(value)]="interests">
|
|
883
|
+
* <kt-checkbox [value]="'sport'" label="Sport" />
|
|
884
|
+
* <kt-checkbox [value]="'musique'" label="Musique" />
|
|
885
|
+
* </kt-checkbox-group>
|
|
886
|
+
* ```
|
|
887
|
+
*/
|
|
888
|
+
class KtCheckbox {
|
|
889
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
890
|
+
/** Groupe parent optionnel : présent ⇒ la case appartient à un `kt-checkbox-group`. */
|
|
891
|
+
group = inject(KtCheckboxGroup, { optional: true });
|
|
892
|
+
/** Autonome : état coché (`boolean`, two-way). En groupe : la valeur d'option représentée. */
|
|
893
|
+
value = model(false, /* @ts-ignore */
|
|
894
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
895
|
+
/** Tri-état visuel (parent d'une arborescence) : propriété DOM `indeterminate`, pas un attribut. */
|
|
896
|
+
indeterminate = input(false, { ...(ngDevMode ? { debugName: "indeterminate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
897
|
+
// --- État poussé par [formField] (mode autonome) ---
|
|
898
|
+
/** État « touché » (two-way), généralement piloté par `[formField]`. @default false */
|
|
899
|
+
touched = model(false, /* @ts-ignore */
|
|
900
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
901
|
+
/** Désactive la case (héritable du groupe parent). @default false */
|
|
902
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
903
|
+
/** Marque la case comme invalide (combiné à `touched`/`dirty` via l'`errorMatcher`). @default false */
|
|
904
|
+
invalid = input(false, /* @ts-ignore */
|
|
905
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
906
|
+
/** Affiche l'astérisque requis et `aria-required`. @default false */
|
|
907
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
908
|
+
/** État « modifié » (entre dans la logique d'affichage des erreurs). @default false */
|
|
909
|
+
dirty = input(false, /* @ts-ignore */
|
|
910
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
911
|
+
/** Erreurs de validation à afficher sous la case. @default [] */
|
|
912
|
+
errors = input([], /* @ts-ignore */
|
|
913
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
914
|
+
/** Attribut `name` du `<input>` natif. @default '' */
|
|
915
|
+
name = input('', /* @ts-ignore */
|
|
916
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
917
|
+
// --- Présentation ---
|
|
918
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. @default undefined */
|
|
919
|
+
id = input(/* @ts-ignore */
|
|
920
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
921
|
+
/** Texte du libellé (remplacé visuellement par un contenu projeté). @default undefined */
|
|
922
|
+
label = input(/* @ts-ignore */
|
|
923
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
924
|
+
/** Texte d'aide affiché sous la case (masqué quand une erreur s'affiche). @default undefined */
|
|
925
|
+
hint = input(/* @ts-ignore */
|
|
926
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
927
|
+
/** Nom accessible (`aria-label`) pour une case sans libellé textuel. @default undefined */
|
|
928
|
+
ariaLabel = input(/* @ts-ignore */
|
|
929
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
930
|
+
/** Stratégie décidant quand afficher les erreurs (sinon celle du `KT_FIELD_CONFIG`). @default undefined */
|
|
931
|
+
errorMatcher = input(/* @ts-ignore */
|
|
932
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
933
|
+
/** Afficher toutes les erreurs au lieu de la première seule. @default KT_FIELD_CONFIG.showAllErrors ?? false */
|
|
934
|
+
showAllErrors = input(this.config?.showAllErrors ?? false, /* @ts-ignore */
|
|
935
|
+
...(ngDevMode ? [{ debugName: "showAllErrors" }] : /* istanbul ignore next */ []));
|
|
936
|
+
inputEl = viewChild.required('input');
|
|
937
|
+
idGen = inject(KtIdGenerator);
|
|
938
|
+
uid = this.idGen.generateId('checkbox');
|
|
939
|
+
baseId = computed(() => this.id() ?? `kt-checkbox-${this.uid}`, /* @ts-ignore */
|
|
940
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
941
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
942
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
943
|
+
errorId = computed(() => `${this.baseId()}-error`, /* @ts-ignore */
|
|
944
|
+
...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
945
|
+
/** Coché : appartenance au groupe le cas échéant, sinon l'état booléen propre.
|
|
946
|
+
(Nommé `isChecked` car `FormValueControl` réserve la propriété `checked`.) */
|
|
947
|
+
isChecked = computed(() => this.group ? this.group.isSelected(this.value()) : Boolean(this.value()), /* @ts-ignore */
|
|
948
|
+
...(ngDevMode ? [{ debugName: "isChecked" }] : /* istanbul ignore next */ []));
|
|
949
|
+
/** Désactivé : hérité du groupe le cas échéant. */
|
|
950
|
+
isDisabled = computed(() => (this.group?.disabled() ?? false) || this.disabled(), /* @ts-ignore */
|
|
951
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
952
|
+
matcher = computed(() => this.errorMatcher() ?? this.config?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
953
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
954
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
955
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
956
|
+
displayedErrors = computed(() => this.showAllErrors() ? this.errors() : this.errors().slice(0, 1), /* @ts-ignore */
|
|
957
|
+
...(ngDevMode ? [{ debugName: "displayedErrors" }] : /* istanbul ignore next */ []));
|
|
958
|
+
resolvedAriaLabel = computed(() => this.ariaLabel() ?? null, /* @ts-ignore */
|
|
959
|
+
...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
960
|
+
describedBy = computed(() => {
|
|
961
|
+
const ids = [];
|
|
962
|
+
if (this.hint() && !this.showInvalid())
|
|
963
|
+
ids.push(this.hintId());
|
|
964
|
+
if (this.showInvalid() && this.errors().length > 0)
|
|
965
|
+
ids.push(this.errorId());
|
|
966
|
+
return ids.length ? ids.join(' ') : null;
|
|
967
|
+
}, /* @ts-ignore */
|
|
968
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
969
|
+
constructor() {
|
|
970
|
+
// `indeterminate` est une PROPRIÉTÉ (non un attribut) : poussée impérativement sur l'input natif.
|
|
971
|
+
effect(() => {
|
|
972
|
+
this.inputEl().nativeElement.indeterminate = this.indeterminate();
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
onChange(event) {
|
|
976
|
+
if (this.isDisabled())
|
|
977
|
+
return;
|
|
978
|
+
const checked = event.target.checked;
|
|
979
|
+
if (this.group) {
|
|
980
|
+
this.group.toggle(this.value(), checked);
|
|
981
|
+
this.group.touched.set(true);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
this.value.set(checked);
|
|
985
|
+
this.touched.set(true);
|
|
986
|
+
}
|
|
987
|
+
onBlur() {
|
|
988
|
+
(this.group?.touched ?? this.touched).set(true);
|
|
989
|
+
}
|
|
990
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
991
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtCheckbox, isStandalone: true, selector: "kt-checkbox", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, indeterminate: { classPropertyName: "indeterminate", publicName: "indeterminate", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null }, showAllErrors: { classPropertyName: "showAllErrors", publicName: "showAllErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
992
|
+
<div
|
|
993
|
+
class="kt-checkbox-field"
|
|
994
|
+
[class.kt-checkbox-field--invalid]="showInvalid()"
|
|
995
|
+
[class.kt-checkbox-field--disabled]="isDisabled()"
|
|
996
|
+
>
|
|
997
|
+
<label class="kt-checkbox">
|
|
998
|
+
<input
|
|
999
|
+
#input
|
|
1000
|
+
type="checkbox"
|
|
1001
|
+
class="kt-checkbox__input"
|
|
1002
|
+
[id]="baseId()"
|
|
1003
|
+
[checked]="isChecked()"
|
|
1004
|
+
[disabled]="isDisabled()"
|
|
1005
|
+
[attr.name]="name() || null"
|
|
1006
|
+
[attr.aria-label]="resolvedAriaLabel()"
|
|
1007
|
+
[attr.aria-describedby]="describedBy()"
|
|
1008
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
1009
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1010
|
+
(change)="onChange($event)"
|
|
1011
|
+
(blur)="onBlur()"
|
|
1012
|
+
/>
|
|
1013
|
+
<span class="kt-checkbox__label">
|
|
1014
|
+
<ng-content>{{ label() }}</ng-content>
|
|
1015
|
+
@if (required()) {
|
|
1016
|
+
<span class="kt-checkbox__required" aria-hidden="true">*</span>
|
|
1017
|
+
}
|
|
1018
|
+
</span>
|
|
1019
|
+
</label>
|
|
1020
|
+
|
|
1021
|
+
@if (hint() && !showInvalid()) {
|
|
1022
|
+
<p [id]="hintId()" class="kt-checkbox-hint">{{ hint() }}</p>
|
|
1023
|
+
}
|
|
1024
|
+
<div [id]="errorId()" class="kt-checkbox-error" aria-live="polite">
|
|
1025
|
+
@if (showInvalid()) {
|
|
1026
|
+
@for (error of displayedErrors(); track $index) {
|
|
1027
|
+
<span class="kt-checkbox-error-message">{{ error.message }}</span>
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
`, isInline: true, styles: ["@layer kt-aaa.components{:host{display:block}.kt-checkbox-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-checkbox{display:flex;align-items:center;gap:var(--field-control-gap, .5rem);inline-size:100%;box-sizing:border-box;min-block-size:var(--checkbox-target-size, 44px);padding:var(--checkbox-padding, 0);border-radius:inherit;cursor:pointer;-webkit-user-select:none;user-select:none}.kt-checkbox__input{appearance:none;-webkit-appearance:none;margin:0;flex:none;box-sizing:border-box;display:grid;place-content:center;font-size:var(--checkbox-size, 1.25rem);inline-size:1em;block-size:1em;border:var(--checkbox-border-width, 2px) solid var(--checkbox-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));border-radius:var(--checkbox-radius, .25rem);background-color:var(--checkbox-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;box-shadow:var(--checkbox-shadow, var(--field-shadow, none));transition:var( --checkbox-transition, background-color .15s cubic-bezier(.4, 0, .2, 1), border-color .15s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-checkbox__input:before{content:\"\";inline-size:.65em;block-size:.65em;transform:scale(0);transform-origin:center;box-shadow:inset 1em 1em var(--checkbox-mark-color, #ffffff);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0%,43% 62%);transition:var(--checkbox-mark-transition, transform .12s ease-in)}.kt-checkbox__input:checked:before,.kt-checkbox__input:indeterminate:before{transform:scale(1);animation:var(--checkbox-mark-animation, kt-checkbox-pop .22s cubic-bezier(.34, 1.56, .64, 1))}.kt-checkbox__input:indeterminate:before{clip-path:inset(42% 12%)}@keyframes kt-checkbox-pop{0%{transform:scale(0)}}.kt-checkbox__input:checked,.kt-checkbox__input:indeterminate{background-color:var(--checkbox-bg-checked, var(--kt-primary, #0842a0));border-color:var(--checkbox-border-color-checked, var(--checkbox-bg-checked, var(--kt-primary, #0842a0)));box-shadow:var( --checkbox-shadow-checked, 0 0 8px color-mix(in srgb, var(--checkbox-bg-checked, var(--kt-primary, #0842a0)) 50%, transparent) )}.kt-checkbox__input:hover:not(:disabled){border-color:var( --checkbox-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)) );box-shadow:var( --checkbox-shadow-hover, var(--field-shadow-hover, var(--checkbox-shadow, var(--field-shadow, none))) )}.kt-checkbox__input:checked:hover:not(:disabled),.kt-checkbox__input:indeterminate:hover:not(:disabled){background-color:var(--checkbox-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black));border-color:var(--checkbox-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black))}.kt-checkbox__input:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0842a0);outline-offset:var(--checkbox-focus-ring-offset, 2px);box-shadow:var(--checkbox-shadow-focus, var(--field-shadow-focus, var(--field-shadow, none)))}.kt-checkbox__input:disabled{cursor:not-allowed;opacity:.5}.kt-checkbox-field--disabled .kt-checkbox{cursor:not-allowed}.kt-checkbox-field--invalid .kt-checkbox__input:not(:checked):not(:indeterminate){border-color:var(--field-error-color, var(--kt-danger, #8c1d18))}.kt-checkbox__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-checkbox-field--disabled .kt-checkbox__label{color:var(--field-hint-color, #5f6368);opacity:.6}.kt-checkbox__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-checkbox-hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-checkbox-error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-checkbox-error-message{animation:var(--field-error-animation, none)}@media(forced-colors:active){.kt-checkbox__input{border-color:CanvasText;background-color:Canvas}.kt-checkbox__input:checked,.kt-checkbox__input:indeterminate{background-color:Highlight;border-color:Highlight}.kt-checkbox__input:before{box-shadow:inset 1em 1em HighlightText}.kt-checkbox__input:disabled{border-color:GrayText}.kt-checkbox__input:focus-visible{outline:2px solid CanvasText;outline-offset:2px}}@media(prefers-reduced-motion:reduce){.kt-checkbox__input,.kt-checkbox__input:before{transition:none;animation:none}.kt-checkbox-error-message{animation:none}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1033
|
+
}
|
|
1034
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtCheckbox, decorators: [{
|
|
1035
|
+
type: Component,
|
|
1036
|
+
args: [{ selector: 'kt-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1037
|
+
<div
|
|
1038
|
+
class="kt-checkbox-field"
|
|
1039
|
+
[class.kt-checkbox-field--invalid]="showInvalid()"
|
|
1040
|
+
[class.kt-checkbox-field--disabled]="isDisabled()"
|
|
1041
|
+
>
|
|
1042
|
+
<label class="kt-checkbox">
|
|
1043
|
+
<input
|
|
1044
|
+
#input
|
|
1045
|
+
type="checkbox"
|
|
1046
|
+
class="kt-checkbox__input"
|
|
1047
|
+
[id]="baseId()"
|
|
1048
|
+
[checked]="isChecked()"
|
|
1049
|
+
[disabled]="isDisabled()"
|
|
1050
|
+
[attr.name]="name() || null"
|
|
1051
|
+
[attr.aria-label]="resolvedAriaLabel()"
|
|
1052
|
+
[attr.aria-describedby]="describedBy()"
|
|
1053
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
1054
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1055
|
+
(change)="onChange($event)"
|
|
1056
|
+
(blur)="onBlur()"
|
|
1057
|
+
/>
|
|
1058
|
+
<span class="kt-checkbox__label">
|
|
1059
|
+
<ng-content>{{ label() }}</ng-content>
|
|
1060
|
+
@if (required()) {
|
|
1061
|
+
<span class="kt-checkbox__required" aria-hidden="true">*</span>
|
|
1062
|
+
}
|
|
1063
|
+
</span>
|
|
1064
|
+
</label>
|
|
1065
|
+
|
|
1066
|
+
@if (hint() && !showInvalid()) {
|
|
1067
|
+
<p [id]="hintId()" class="kt-checkbox-hint">{{ hint() }}</p>
|
|
1068
|
+
}
|
|
1069
|
+
<div [id]="errorId()" class="kt-checkbox-error" aria-live="polite">
|
|
1070
|
+
@if (showInvalid()) {
|
|
1071
|
+
@for (error of displayedErrors(); track $index) {
|
|
1072
|
+
<span class="kt-checkbox-error-message">{{ error.message }}</span>
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
`, styles: ["@layer kt-aaa.components{:host{display:block}.kt-checkbox-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-checkbox{display:flex;align-items:center;gap:var(--field-control-gap, .5rem);inline-size:100%;box-sizing:border-box;min-block-size:var(--checkbox-target-size, 44px);padding:var(--checkbox-padding, 0);border-radius:inherit;cursor:pointer;-webkit-user-select:none;user-select:none}.kt-checkbox__input{appearance:none;-webkit-appearance:none;margin:0;flex:none;box-sizing:border-box;display:grid;place-content:center;font-size:var(--checkbox-size, 1.25rem);inline-size:1em;block-size:1em;border:var(--checkbox-border-width, 2px) solid var(--checkbox-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));border-radius:var(--checkbox-radius, .25rem);background-color:var(--checkbox-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;box-shadow:var(--checkbox-shadow, var(--field-shadow, none));transition:var( --checkbox-transition, background-color .15s cubic-bezier(.4, 0, .2, 1), border-color .15s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-checkbox__input:before{content:\"\";inline-size:.65em;block-size:.65em;transform:scale(0);transform-origin:center;box-shadow:inset 1em 1em var(--checkbox-mark-color, #ffffff);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0%,43% 62%);transition:var(--checkbox-mark-transition, transform .12s ease-in)}.kt-checkbox__input:checked:before,.kt-checkbox__input:indeterminate:before{transform:scale(1);animation:var(--checkbox-mark-animation, kt-checkbox-pop .22s cubic-bezier(.34, 1.56, .64, 1))}.kt-checkbox__input:indeterminate:before{clip-path:inset(42% 12%)}@keyframes kt-checkbox-pop{0%{transform:scale(0)}}.kt-checkbox__input:checked,.kt-checkbox__input:indeterminate{background-color:var(--checkbox-bg-checked, var(--kt-primary, #0842a0));border-color:var(--checkbox-border-color-checked, var(--checkbox-bg-checked, var(--kt-primary, #0842a0)));box-shadow:var( --checkbox-shadow-checked, 0 0 8px color-mix(in srgb, var(--checkbox-bg-checked, var(--kt-primary, #0842a0)) 50%, transparent) )}.kt-checkbox__input:hover:not(:disabled){border-color:var( --checkbox-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)) );box-shadow:var( --checkbox-shadow-hover, var(--field-shadow-hover, var(--checkbox-shadow, var(--field-shadow, none))) )}.kt-checkbox__input:checked:hover:not(:disabled),.kt-checkbox__input:indeterminate:hover:not(:disabled){background-color:var(--checkbox-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black));border-color:var(--checkbox-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black))}.kt-checkbox__input:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0842a0);outline-offset:var(--checkbox-focus-ring-offset, 2px);box-shadow:var(--checkbox-shadow-focus, var(--field-shadow-focus, var(--field-shadow, none)))}.kt-checkbox__input:disabled{cursor:not-allowed;opacity:.5}.kt-checkbox-field--disabled .kt-checkbox{cursor:not-allowed}.kt-checkbox-field--invalid .kt-checkbox__input:not(:checked):not(:indeterminate){border-color:var(--field-error-color, var(--kt-danger, #8c1d18))}.kt-checkbox__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-checkbox-field--disabled .kt-checkbox__label{color:var(--field-hint-color, #5f6368);opacity:.6}.kt-checkbox__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-checkbox-hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-checkbox-error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-checkbox-error-message{animation:var(--field-error-animation, none)}@media(forced-colors:active){.kt-checkbox__input{border-color:CanvasText;background-color:Canvas}.kt-checkbox__input:checked,.kt-checkbox__input:indeterminate{background-color:Highlight;border-color:Highlight}.kt-checkbox__input:before{box-shadow:inset 1em 1em HighlightText}.kt-checkbox__input:disabled{border-color:GrayText}.kt-checkbox__input:focus-visible{outline:2px solid CanvasText;outline-offset:2px}}@media(prefers-reduced-motion:reduce){.kt-checkbox__input,.kt-checkbox__input:before{transition:none;animation:none}.kt-checkbox-error-message{animation:none}}}\n"] }]
|
|
1078
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], showAllErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAllErrors", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }] } });
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Groupe de boutons radio (Radio Group) conforme à l'ARIA APG / RGAA.
|
|
1082
|
+
* Orchestrateur : porte la valeur (`FormValueControl<V | null>`), génère le `name` partagé et la
|
|
1083
|
+
* sémantique de groupe (`role="radiogroup"`, légende, `aria-required`/`aria-invalid`, erreurs).
|
|
1084
|
+
*
|
|
1085
|
+
* Le clavier (roving tabindex + flèches qui déplacent ET cochent) est laissé au NATIF : les
|
|
1086
|
+
* `<input type="radio">` des `kt-radio` enfants partagent le `name` du groupe — rien à réimplémenter.
|
|
1087
|
+
* Les options sont fournies par le dev (boucle `@for` ou déclaration en dur) ; `compareWith` gère
|
|
1088
|
+
* l'égalité quand `value` est un objet.
|
|
1089
|
+
*
|
|
1090
|
+
* @example
|
|
1091
|
+
* ```html
|
|
1092
|
+
* <kt-radio-group label="Civilité" [(value)]="civility" required>
|
|
1093
|
+
* <kt-radio [value]="'mme'" label="Madame" />
|
|
1094
|
+
* <kt-radio [value]="'m'" label="Monsieur" />
|
|
1095
|
+
* <kt-radio [value]="'autre'" label="Autre" />
|
|
1096
|
+
* </kt-radio-group>
|
|
1097
|
+
* ```
|
|
1098
|
+
*/
|
|
1099
|
+
class KtRadioGroup {
|
|
1100
|
+
config = inject(KT_FIELD_CONFIG, { optional: true });
|
|
1101
|
+
/** Valeur sélectionnée (two-way), ou `null` si aucune option n'est cochée. @default null */
|
|
1102
|
+
value = model(null, /* @ts-ignore */
|
|
1103
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1104
|
+
// --- État poussé par [formField] ---
|
|
1105
|
+
/** État « touché » (two-way), généralement piloté par `[formField]`. @default false */
|
|
1106
|
+
touched = model(false, /* @ts-ignore */
|
|
1107
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
1108
|
+
/** Désactive le groupe entier (hérité par chaque radio enfant). @default false */
|
|
1109
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1110
|
+
/** Marque le groupe comme invalide (combiné à `touched`/`dirty` via l'`errorMatcher`). @default false */
|
|
1111
|
+
invalid = input(false, /* @ts-ignore */
|
|
1112
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
1113
|
+
/** Affiche l'astérisque requis et `aria-required` sur le groupe. @default false */
|
|
1114
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1115
|
+
/** État « modifié » (entre dans la logique d'affichage des erreurs). @default false */
|
|
1116
|
+
dirty = input(false, /* @ts-ignore */
|
|
1117
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
1118
|
+
/** Erreurs de validation à afficher sous le groupe. @default [] */
|
|
1119
|
+
errors = input([], /* @ts-ignore */
|
|
1120
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
1121
|
+
/** Base du `name` partagé par les radios enfants (sinon auto-généré). @default '' */
|
|
1122
|
+
name = input('', /* @ts-ignore */
|
|
1123
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
1124
|
+
// --- Présentation ---
|
|
1125
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. @default undefined */
|
|
1126
|
+
id = input(/* @ts-ignore */
|
|
1127
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
1128
|
+
/** Légende du groupe (`aria-labelledby`). @default undefined */
|
|
1129
|
+
label = input(/* @ts-ignore */
|
|
1130
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1131
|
+
/** Texte d'aide affiché sous le groupe (masqué quand une erreur s'affiche). @default undefined */
|
|
1132
|
+
hint = input(/* @ts-ignore */
|
|
1133
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
1134
|
+
/** Nom accessible (`aria-label`) du groupe quand `label` est absent. @default undefined */
|
|
1135
|
+
ariaLabel = input(/* @ts-ignore */
|
|
1136
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
1137
|
+
/** Stratégie décidant quand afficher les erreurs (sinon celle du `KT_FIELD_CONFIG`). @default undefined */
|
|
1138
|
+
errorMatcher = input(/* @ts-ignore */
|
|
1139
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
1140
|
+
/** Afficher toutes les erreurs au lieu de la première seule. @default KT_FIELD_CONFIG.showAllErrors ?? false */
|
|
1141
|
+
showAllErrors = input(this.config?.showAllErrors ?? false, /* @ts-ignore */
|
|
1142
|
+
...(ngDevMode ? [{ debugName: "showAllErrors" }] : /* istanbul ignore next */ []));
|
|
1143
|
+
/** Égalité des valeurs en mode objet (défaut : identité `===`). Seul rescapé du contrat select. */
|
|
1144
|
+
compareWith = input(/* @ts-ignore */
|
|
1145
|
+
...(ngDevMode ? [undefined, { debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
1146
|
+
idGen = inject(KtIdGenerator);
|
|
1147
|
+
uid = this.idGen.generateId('radio-group');
|
|
1148
|
+
baseId = computed(() => this.id() ?? `kt-radio-group-${this.uid}`, /* @ts-ignore */
|
|
1149
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
1150
|
+
/** `name` partagé par les radios enfants → regroupement clavier NATIF (roving + flèches). */
|
|
1151
|
+
groupName = computed(() => this.name() || `${this.baseId()}-name`, /* @ts-ignore */
|
|
1152
|
+
...(ngDevMode ? [{ debugName: "groupName" }] : /* istanbul ignore next */ []));
|
|
1153
|
+
labelId = computed(() => `${this.baseId()}-label`, /* @ts-ignore */
|
|
1154
|
+
...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
1155
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
1156
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
1157
|
+
errorId = computed(() => `${this.baseId()}-error`, /* @ts-ignore */
|
|
1158
|
+
...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
1159
|
+
matcher = computed(() => this.errorMatcher() ?? this.config?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
1160
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
1161
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
1162
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
1163
|
+
displayedErrors = computed(() => this.showAllErrors() ? this.errors() : this.errors().slice(0, 1), /* @ts-ignore */
|
|
1164
|
+
...(ngDevMode ? [{ debugName: "displayedErrors" }] : /* istanbul ignore next */ []));
|
|
1165
|
+
resolvedAriaLabel = computed(() => this.ariaLabel() ?? null, /* @ts-ignore */
|
|
1166
|
+
...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
1167
|
+
describedBy = computed(() => {
|
|
1168
|
+
const ids = [];
|
|
1169
|
+
if (this.hint() && !this.showInvalid())
|
|
1170
|
+
ids.push(this.hintId());
|
|
1171
|
+
if (this.showInvalid() && this.errors().length > 0)
|
|
1172
|
+
ids.push(this.errorId());
|
|
1173
|
+
return ids.length ? ids.join(' ') : null;
|
|
1174
|
+
}, /* @ts-ignore */
|
|
1175
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1176
|
+
comparator = computed(() => this.compareWith() ?? ((a, b) => a === b), /* @ts-ignore */
|
|
1177
|
+
...(ngDevMode ? [{ debugName: "comparator" }] : /* istanbul ignore next */ []));
|
|
1178
|
+
/** Le `value` d'un enfant correspond-il à la sélection courante ? (lu dans le `computed` enfant) */
|
|
1179
|
+
isSelected(radioValue) {
|
|
1180
|
+
const v = this.value();
|
|
1181
|
+
if (v === null || v === undefined)
|
|
1182
|
+
return false;
|
|
1183
|
+
return this.comparator()(radioValue, v);
|
|
1184
|
+
}
|
|
1185
|
+
/** Commit d'une sélection émis par un enfant au `change` natif. */
|
|
1186
|
+
select(radioValue) {
|
|
1187
|
+
if (this.disabled())
|
|
1188
|
+
return;
|
|
1189
|
+
this.value.set(radioValue);
|
|
1190
|
+
this.touched.set(true);
|
|
1191
|
+
}
|
|
1192
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1193
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtRadioGroup, isStandalone: true, selector: "kt-radio-group", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null }, showAllErrors: { classPropertyName: "showAllErrors", publicName: "showAllErrors", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: `
|
|
1194
|
+
<div
|
|
1195
|
+
class="kt-radio-group-field"
|
|
1196
|
+
[class.kt-radio-group-field--invalid]="showInvalid()"
|
|
1197
|
+
[class.kt-radio-group-field--disabled]="disabled()"
|
|
1198
|
+
role="radiogroup"
|
|
1199
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
1200
|
+
[attr.aria-label]="!label() ? resolvedAriaLabel() : null"
|
|
1201
|
+
[attr.aria-describedby]="describedBy()"
|
|
1202
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1203
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
1204
|
+
>
|
|
1205
|
+
@if (label(); as labelText) {
|
|
1206
|
+
<span [id]="labelId()" class="kt-radio-group__legend">
|
|
1207
|
+
{{ labelText }}
|
|
1208
|
+
@if (required()) {
|
|
1209
|
+
<span class="kt-radio-group__required" aria-hidden="true">*</span>
|
|
1210
|
+
}
|
|
1211
|
+
</span>
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
<div class="kt-radio-group__options">
|
|
1215
|
+
<ng-content></ng-content>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
@if (hint() && !showInvalid()) {
|
|
1219
|
+
<p [id]="hintId()" class="kt-radio-group__hint">{{ hint() }}</p>
|
|
1220
|
+
}
|
|
1221
|
+
<div [id]="errorId()" class="kt-radio-group__error" aria-live="polite">
|
|
1222
|
+
@if (showInvalid()) {
|
|
1223
|
+
@for (error of displayedErrors(); track $index) {
|
|
1224
|
+
<span class="kt-radio-group__error-message">{{ error.message }}</span>
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
`, isInline: true, styles: ["@layer kt-aaa.components{:host{display:block}.kt-radio-group-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-radio-group__legend{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-radio-group__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-radio-group__options{display:flex;flex-direction:var(--radio-group-direction, column);flex-wrap:wrap;gap:var(--radio-group-gap, .25rem)}.kt-radio-group__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-radio-group__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-radio-group__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-radio-group__error-message{animation:none}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1230
|
+
}
|
|
1231
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtRadioGroup, decorators: [{
|
|
1232
|
+
type: Component,
|
|
1233
|
+
args: [{ selector: 'kt-radio-group', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1234
|
+
<div
|
|
1235
|
+
class="kt-radio-group-field"
|
|
1236
|
+
[class.kt-radio-group-field--invalid]="showInvalid()"
|
|
1237
|
+
[class.kt-radio-group-field--disabled]="disabled()"
|
|
1238
|
+
role="radiogroup"
|
|
1239
|
+
[attr.aria-labelledby]="label() ? labelId() : null"
|
|
1240
|
+
[attr.aria-label]="!label() ? resolvedAriaLabel() : null"
|
|
1241
|
+
[attr.aria-describedby]="describedBy()"
|
|
1242
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1243
|
+
[attr.aria-invalid]="showInvalid() ? 'true' : null"
|
|
1244
|
+
>
|
|
1245
|
+
@if (label(); as labelText) {
|
|
1246
|
+
<span [id]="labelId()" class="kt-radio-group__legend">
|
|
1247
|
+
{{ labelText }}
|
|
1248
|
+
@if (required()) {
|
|
1249
|
+
<span class="kt-radio-group__required" aria-hidden="true">*</span>
|
|
1250
|
+
}
|
|
1251
|
+
</span>
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
<div class="kt-radio-group__options">
|
|
1255
|
+
<ng-content></ng-content>
|
|
1256
|
+
</div>
|
|
1257
|
+
|
|
1258
|
+
@if (hint() && !showInvalid()) {
|
|
1259
|
+
<p [id]="hintId()" class="kt-radio-group__hint">{{ hint() }}</p>
|
|
1260
|
+
}
|
|
1261
|
+
<div [id]="errorId()" class="kt-radio-group__error" aria-live="polite">
|
|
1262
|
+
@if (showInvalid()) {
|
|
1263
|
+
@for (error of displayedErrors(); track $index) {
|
|
1264
|
+
<span class="kt-radio-group__error-message">{{ error.message }}</span>
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
`, styles: ["@layer kt-aaa.components{:host{display:block}.kt-radio-group-field{display:flex;flex-direction:column;gap:var(--field-gap, .375rem)}.kt-radio-group__legend{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-radio-group__required{margin-inline-start:.125rem;color:var(--field-required-color, var(--kt-danger, #8c1d18))}.kt-radio-group__options{display:flex;flex-direction:var(--radio-group-direction, column);flex-wrap:wrap;gap:var(--radio-group-gap, .25rem)}.kt-radio-group__hint{margin:0;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}.kt-radio-group__error{margin:0;display:flex;flex-direction:column;font-size:var(--field-hint-font-size, .8125rem);color:var(--field-error-color, #8c1d18)}.kt-radio-group__error-message{animation:var(--field-error-animation, none)}@media(prefers-reduced-motion:reduce){.kt-radio-group__error-message{animation:none}}}\n"] }]
|
|
1270
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], showAllErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAllErrors", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }] } });
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Bouton radio individuel (enfant déclaratif de `kt-radio-group`).
|
|
1274
|
+
* `<input type="radio">` NATIF partageant le `name` du groupe : clavier (roving + flèches) gratuit.
|
|
1275
|
+
* Le libellé suit la même règle que la checkbox : `label` texte par défaut, contenu projeté en
|
|
1276
|
+
* override visuel (carte sélectionnable), `ariaLabel` pour le nom accessible d'une option non-textuelle.
|
|
1277
|
+
*
|
|
1278
|
+
* @example
|
|
1279
|
+
* ```html
|
|
1280
|
+
* <kt-radio-group label="Civilité" [(value)]="civility">
|
|
1281
|
+
* <kt-radio [value]="'mme'" label="Madame" />
|
|
1282
|
+
* <kt-radio [value]="'m'" label="Monsieur" />
|
|
1283
|
+
* </kt-radio-group>
|
|
1284
|
+
* ```
|
|
1285
|
+
*/
|
|
1286
|
+
class KtRadio {
|
|
1287
|
+
group = inject(KtRadioGroup);
|
|
1288
|
+
/** Valeur d'option représentée par ce radio (sélectionnée ⇒ devient la valeur du groupe). @default (requis) */
|
|
1289
|
+
value = input.required(/* @ts-ignore */
|
|
1290
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1291
|
+
/** Texte du libellé (remplacé visuellement par un contenu projeté). @default undefined */
|
|
1292
|
+
label = input(/* @ts-ignore */
|
|
1293
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1294
|
+
/** Texte d'aide affiché sous le radio. @default undefined */
|
|
1295
|
+
hint = input(/* @ts-ignore */
|
|
1296
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
1297
|
+
/** Nom accessible (`aria-label`) pour une option sans libellé textuel. @default undefined */
|
|
1298
|
+
ariaLabel = input(/* @ts-ignore */
|
|
1299
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
1300
|
+
/** Désactive ce radio (combiné à l'état désactivé du groupe). @default false */
|
|
1301
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1302
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré, anti-collision. @default undefined */
|
|
1303
|
+
id = input(/* @ts-ignore */
|
|
1304
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
1305
|
+
idGen = inject(KtIdGenerator);
|
|
1306
|
+
uid = this.idGen.generateId('radio');
|
|
1307
|
+
baseId = computed(() => this.id() ?? `kt-radio-${this.uid}`, /* @ts-ignore */
|
|
1308
|
+
...(ngDevMode ? [{ debugName: "baseId" }] : /* istanbul ignore next */ []));
|
|
1309
|
+
hintId = computed(() => `${this.baseId()}-hint`, /* @ts-ignore */
|
|
1310
|
+
...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
checked = computed(() => this.group.isSelected(this.value()), /* @ts-ignore */
|
|
1312
|
+
...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
|
|
1313
|
+
isDisabled = computed(() => this.group.disabled() || this.disabled(), /* @ts-ignore */
|
|
1314
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1315
|
+
onChange() {
|
|
1316
|
+
this.group.select(this.value());
|
|
1317
|
+
}
|
|
1318
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtRadio, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1319
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtRadio, isStandalone: true, selector: "kt-radio", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1320
|
+
<label class="kt-radio">
|
|
1321
|
+
<input
|
|
1322
|
+
type="radio"
|
|
1323
|
+
class="kt-radio__input"
|
|
1324
|
+
[id]="baseId()"
|
|
1325
|
+
[name]="group.groupName()"
|
|
1326
|
+
[checked]="checked()"
|
|
1327
|
+
[disabled]="isDisabled()"
|
|
1328
|
+
[attr.aria-label]="ariaLabel() ?? null"
|
|
1329
|
+
[attr.aria-describedby]="hint() ? hintId() : null"
|
|
1330
|
+
(change)="onChange()"
|
|
1331
|
+
(blur)="group.touched.set(true)"
|
|
1332
|
+
/>
|
|
1333
|
+
<span class="kt-radio__label">
|
|
1334
|
+
<ng-content>{{ label() }}</ng-content>
|
|
1335
|
+
</span>
|
|
1336
|
+
</label>
|
|
1337
|
+
|
|
1338
|
+
@if (hint(); as hintText) {
|
|
1339
|
+
<p [id]="hintId()" class="kt-radio__hint">{{ hintText }}</p>
|
|
1340
|
+
}
|
|
1341
|
+
`, isInline: true, styles: ["@layer kt-aaa.components{:host{display:block}.kt-radio{display:flex;align-items:center;gap:var(--field-control-gap, .5rem);inline-size:100%;box-sizing:border-box;min-block-size:var(--radio-target-size, 44px);padding:var(--radio-padding, 0);border-radius:inherit;cursor:pointer;-webkit-user-select:none;user-select:none}.kt-radio__input{appearance:none;-webkit-appearance:none;margin:0;flex:none;box-sizing:border-box;display:grid;place-content:center;font-size:var(--radio-size, 1.25rem);inline-size:1em;block-size:1em;border:var(--radio-border-width, 2px) solid var(--radio-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));border-radius:50%;background-color:var(--radio-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;box-shadow:var(--radio-shadow, var(--field-shadow, none));transition:var( --radio-transition, background-color .15s cubic-bezier(.4, 0, .2, 1), border-color .15s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-radio__input:before{content:\"\";inline-size:.5em;block-size:.5em;border-radius:50%;transform:scale(0);transform-origin:center;box-shadow:inset 1em 1em var(--radio-dot-color, #ffffff);transition:var(--radio-dot-transition, transform .12s ease-in)}.kt-radio__input:checked:before{transform:scale(1);animation:var(--radio-dot-animation, kt-radio-pop .22s cubic-bezier(.34, 1.56, .64, 1))}@keyframes kt-radio-pop{0%{transform:scale(0)}}.kt-radio__input:checked{background-color:var(--radio-bg-checked, var(--kt-primary, #0842a0));border-color:var(--radio-border-color-checked, var(--radio-bg-checked, var(--kt-primary, #0842a0)));box-shadow:var( --radio-shadow-checked, 0 0 8px color-mix(in srgb, var(--radio-bg-checked, var(--kt-primary, #0842a0)) 50%, transparent) )}.kt-radio__input:hover:not(:disabled){border-color:var(--radio-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)));box-shadow:var(--radio-shadow-hover, var(--field-shadow-hover, var(--radio-shadow, var(--field-shadow, none))))}.kt-radio__input:checked:hover:not(:disabled){background-color:var(--radio-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black));border-color:var(--radio-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black))}.kt-radio__input:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0842a0);outline-offset:var(--radio-focus-ring-offset, 2px);box-shadow:var(--radio-shadow-focus, var(--field-shadow-focus, var(--field-shadow, none)))}.kt-radio__input:disabled{cursor:not-allowed;opacity:.5}.kt-radio:has(.kt-radio__input:disabled){cursor:not-allowed}.kt-radio__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-radio:has(.kt-radio__input:disabled) .kt-radio__label{color:var(--field-hint-color, #5f6368);opacity:.6}.kt-radio__hint{margin:0;margin-inline-start:calc(var(--radio-size, 1.25rem) + var(--field-control-gap, .5rem));font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}@media(forced-colors:active){.kt-radio__input{border-color:CanvasText;background-color:Canvas}.kt-radio__input:checked{border-color:Highlight;background-color:Canvas}.kt-radio__input:before{box-shadow:inset 1em 1em Highlight}.kt-radio__input:disabled{border-color:GrayText}.kt-radio__input:focus-visible{outline:2px solid CanvasText;outline-offset:2px}}@media(prefers-reduced-motion:reduce){.kt-radio__input,.kt-radio__input:before{transition:none;animation:none}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1342
|
+
}
|
|
1343
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtRadio, decorators: [{
|
|
1344
|
+
type: Component,
|
|
1345
|
+
args: [{ selector: 'kt-radio', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1346
|
+
<label class="kt-radio">
|
|
1347
|
+
<input
|
|
1348
|
+
type="radio"
|
|
1349
|
+
class="kt-radio__input"
|
|
1350
|
+
[id]="baseId()"
|
|
1351
|
+
[name]="group.groupName()"
|
|
1352
|
+
[checked]="checked()"
|
|
1353
|
+
[disabled]="isDisabled()"
|
|
1354
|
+
[attr.aria-label]="ariaLabel() ?? null"
|
|
1355
|
+
[attr.aria-describedby]="hint() ? hintId() : null"
|
|
1356
|
+
(change)="onChange()"
|
|
1357
|
+
(blur)="group.touched.set(true)"
|
|
1358
|
+
/>
|
|
1359
|
+
<span class="kt-radio__label">
|
|
1360
|
+
<ng-content>{{ label() }}</ng-content>
|
|
1361
|
+
</span>
|
|
1362
|
+
</label>
|
|
1363
|
+
|
|
1364
|
+
@if (hint(); as hintText) {
|
|
1365
|
+
<p [id]="hintId()" class="kt-radio__hint">{{ hintText }}</p>
|
|
1366
|
+
}
|
|
1367
|
+
`, styles: ["@layer kt-aaa.components{:host{display:block}.kt-radio{display:flex;align-items:center;gap:var(--field-control-gap, .5rem);inline-size:100%;box-sizing:border-box;min-block-size:var(--radio-target-size, 44px);padding:var(--radio-padding, 0);border-radius:inherit;cursor:pointer;-webkit-user-select:none;user-select:none}.kt-radio__input{appearance:none;-webkit-appearance:none;margin:0;flex:none;box-sizing:border-box;display:grid;place-content:center;font-size:var(--radio-size, 1.25rem);inline-size:1em;block-size:1em;border:var(--radio-border-width, 2px) solid var(--radio-border-color, var(--field-border-color, var(--kt-outline, #c4c7c5)));border-radius:50%;background-color:var(--radio-bg, var(--field-bg, var(--kt-surface, #ffffff)));cursor:pointer;box-shadow:var(--radio-shadow, var(--field-shadow, none));transition:var( --radio-transition, background-color .15s cubic-bezier(.4, 0, .2, 1), border-color .15s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease )}.kt-radio__input:before{content:\"\";inline-size:.5em;block-size:.5em;border-radius:50%;transform:scale(0);transform-origin:center;box-shadow:inset 1em 1em var(--radio-dot-color, #ffffff);transition:var(--radio-dot-transition, transform .12s ease-in)}.kt-radio__input:checked:before{transform:scale(1);animation:var(--radio-dot-animation, kt-radio-pop .22s cubic-bezier(.34, 1.56, .64, 1))}@keyframes kt-radio-pop{0%{transform:scale(0)}}.kt-radio__input:checked{background-color:var(--radio-bg-checked, var(--kt-primary, #0842a0));border-color:var(--radio-border-color-checked, var(--radio-bg-checked, var(--kt-primary, #0842a0)));box-shadow:var( --radio-shadow-checked, 0 0 8px color-mix(in srgb, var(--radio-bg-checked, var(--kt-primary, #0842a0)) 50%, transparent) )}.kt-radio__input:hover:not(:disabled){border-color:var(--radio-border-color-hover, var(--field-border-color-hover, var(--kt-outline-strong, #5f6368)));box-shadow:var(--radio-shadow-hover, var(--field-shadow-hover, var(--radio-shadow, var(--field-shadow, none))))}.kt-radio__input:checked:hover:not(:disabled){background-color:var(--radio-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black));border-color:var(--radio-bg-checked-hover, color-mix(in srgb, var(--kt-primary, #0842a0) 90%, black))}.kt-radio__input:focus-visible{outline:var(--kt-focus-ring-width, 2px) solid var(--kt-focus-ring-color, #0842a0);outline-offset:var(--radio-focus-ring-offset, 2px);box-shadow:var(--radio-shadow-focus, var(--field-shadow-focus, var(--field-shadow, none)))}.kt-radio__input:disabled{cursor:not-allowed;opacity:.5}.kt-radio:has(.kt-radio__input:disabled){cursor:not-allowed}.kt-radio__label{font-size:var(--field-label-font-size, .875rem);font-weight:var(--field-label-weight, 500);color:var(--field-label-color, inherit)}.kt-radio:has(.kt-radio__input:disabled) .kt-radio__label{color:var(--field-hint-color, #5f6368);opacity:.6}.kt-radio__hint{margin:0;margin-inline-start:calc(var(--radio-size, 1.25rem) + var(--field-control-gap, .5rem));font-size:var(--field-hint-font-size, .8125rem);color:var(--field-hint-color, #474747)}@media(forced-colors:active){.kt-radio__input{border-color:CanvasText;background-color:Canvas}.kt-radio__input:checked{border-color:Highlight;background-color:Canvas}.kt-radio__input:before{box-shadow:inset 1em 1em Highlight}.kt-radio__input:disabled{border-color:GrayText}.kt-radio__input:focus-visible{outline:2px solid CanvasText;outline-offset:2px}}@media(prefers-reduced-motion:reduce){.kt-radio__input,.kt-radio__input:before{transition:none;animation:none}}}\n"] }]
|
|
1368
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
|
|
1369
|
+
|
|
1370
|
+
const KT_SELECT_CONFIG = new InjectionToken('KT_SELECT_CONFIG');
|
|
1371
|
+
|
|
1372
|
+
/** Normalise une clé/fonction en fonction d'accès, avec repli si non fourni. */
|
|
1373
|
+
function accessor(key, fallback) {
|
|
1374
|
+
if (key === undefined)
|
|
1375
|
+
return fallback;
|
|
1376
|
+
if (typeof key === 'function')
|
|
1377
|
+
return key;
|
|
1378
|
+
return (item) => item[key];
|
|
1379
|
+
}
|
|
1380
|
+
/** Libellé par défaut : `label`/`name` pour un objet, sinon `String(item)`. */
|
|
1381
|
+
function defaultLabel(item) {
|
|
1382
|
+
if (item !== null && typeof item === 'object') {
|
|
1383
|
+
const o = item;
|
|
1384
|
+
const v = o['label'] ?? o['name'];
|
|
1385
|
+
if (v !== undefined && v !== null)
|
|
1386
|
+
return String(v);
|
|
1387
|
+
}
|
|
1388
|
+
return String(item);
|
|
1389
|
+
}
|
|
1390
|
+
/** Identité par défaut : `id`/`value` pour un objet, sinon l'item lui-même (primitif). */
|
|
1391
|
+
function defaultIdentity(item) {
|
|
1392
|
+
if (item !== null && typeof item === 'object') {
|
|
1393
|
+
const o = item;
|
|
1394
|
+
return o['id'] ?? o['value'] ?? item;
|
|
1395
|
+
}
|
|
1396
|
+
return item;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** Annonce différée du nombre de résultats : ~500 ms après la dernière frappe (anti-spam SR). */
|
|
1400
|
+
const ANNOUNCE_DELAY_MS = 500;
|
|
1401
|
+
/** Normalisation du filtre par défaut : insensible à la casse ET aux accents
|
|
1402
|
+
(décomposition NFD puis suppression des diacritiques combinants). */
|
|
1403
|
+
function normalizeForFilter(s) {
|
|
1404
|
+
return s
|
|
1405
|
+
.normalize('NFD')
|
|
1406
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1407
|
+
.toLowerCase();
|
|
1408
|
+
}
|
|
1409
|
+
/** Annonce par défaut du nombre de résultats (lib neutre : anglais, comme `emptyText`). */
|
|
1410
|
+
function defaultFilterResultsText(count) {
|
|
1411
|
+
return count === 1 ? '1 result' : `${count} results`;
|
|
1412
|
+
}
|
|
1413
|
+
function defaultTruncatedResultsText(max, total) {
|
|
1414
|
+
return `Showing first ${max} results of ${total}. Refine your search to see more.`;
|
|
1415
|
+
}
|
|
1416
|
+
function defaultTruncatedResultsAnnouncement(max, total) {
|
|
1417
|
+
return `${max} results displayed out of ${total}. Refine your search to see more.`;
|
|
1418
|
+
}
|
|
1419
|
+
/** Base partagée des sélecteurs à liste fermée (Select single et MultiSelect), bâtis sur
|
|
1420
|
+
`@angular/aria` (Combobox + Listbox) : options et dérivation (clé/fonction), état poussé
|
|
1421
|
+
par `[formField]`, popup Popover responsive (dropdown ancré desktop ↔ bottom-sheet
|
|
1422
|
+
téléphone, drag-to-dismiss), filtrage opt-in avec live region, clavier commun (Échap
|
|
1423
|
+
global, Tab APG depuis le filtre). La VALEUR (`V | null` vs `V[]`) et `selectionChange`
|
|
1424
|
+
restent dans les sous-classes : leurs types divergent, chacune implémente seule
|
|
1425
|
+
`FormValueControl<…>`. Même pattern d'héritage que `BaseInputField`.
|
|
1426
|
+
|
|
1427
|
+
@template T Type d'une option (élément du tableau `options`).
|
|
1428
|
+
@template V Type de la valeur émise par le contrôle (défaut `T` : l'objet entier ;
|
|
1429
|
+
ou la clé extraite quand `optionValue` est fourni). */
|
|
1430
|
+
class KtBaseSelect {
|
|
1431
|
+
fieldConfig = inject(KT_FIELD_CONFIG, { optional: true });
|
|
1432
|
+
config = inject(KT_SELECT_CONFIG, { optional: true });
|
|
1433
|
+
host = inject(ElementRef);
|
|
1434
|
+
ngZone = inject(NgZone);
|
|
1435
|
+
doc = inject(DOCUMENT);
|
|
1436
|
+
platformId = inject(PLATFORM_ID);
|
|
1437
|
+
destroyRef = inject(DestroyRef);
|
|
1438
|
+
wasExpanded = false;
|
|
1439
|
+
// --- Données / dérivation (clé OU fonction, défauts intelligents) ---
|
|
1440
|
+
/** Liste des options affichées (source de vérité du contrôle). */
|
|
1441
|
+
options = input.required(/* @ts-ignore */
|
|
1442
|
+
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1443
|
+
/** Libellé affiché d'une option : clé OU fonction. Défaut : `label`/`name`, sinon `String(option)`. */
|
|
1444
|
+
optionLabel = input(/* @ts-ignore */
|
|
1445
|
+
...(ngDevMode ? [undefined, { debugName: "optionLabel" }] : /* istanbul ignore next */ []));
|
|
1446
|
+
/** Dérive la valeur de sortie ET l'identité d'une option.
|
|
1447
|
+
- Fourni (clé/fonction) ⇒ `value` = la/les clé(s) extraite(s).
|
|
1448
|
+
- Omis ⇒ `value` = l'objet entier ; l'identité interne dérive de `id`/`value`. */
|
|
1449
|
+
optionValue = input(/* @ts-ignore */
|
|
1450
|
+
...(ngDevMode ? [undefined, { debugName: "optionValue" }] : /* istanbul ignore next */ []));
|
|
1451
|
+
/** Désactivation d'une option : clé OU fonction renvoyant un booléen. Défaut : aucune option désactivée. */
|
|
1452
|
+
optionDisabled = input(/* @ts-ignore */
|
|
1453
|
+
...(ngDevMode ? [undefined, { debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
|
|
1454
|
+
/** Égalité des options en mode objet (présélection). Défaut : comparaison par identité (`id`/`value`). */
|
|
1455
|
+
compareWith = input(/* @ts-ignore */
|
|
1456
|
+
...(ngDevMode ? [undefined, { debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
1457
|
+
/** Émis au clic sur le bouton d'aide contextuelle. */
|
|
1458
|
+
helpClick = output();
|
|
1459
|
+
// --- État poussé par [formField] (même contrat que BaseInputField) ---
|
|
1460
|
+
/** Le champ a-t-il été visité ? Poussé par `[formField]` ; passe à `true` à la première sélection. @default false */
|
|
1461
|
+
touched = model(false, /* @ts-ignore */
|
|
1462
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
1463
|
+
/** Champ désactivé (trigger inerte). Poussé par `[formField]`. @default false */
|
|
1464
|
+
disabled = input(false, /* @ts-ignore */
|
|
1465
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1466
|
+
/** Champ en lecture seule (sélection figée, valeur affichée). Poussé par `[formField]`. @default false */
|
|
1467
|
+
readonly = input(false, /* @ts-ignore */
|
|
1468
|
+
...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
1469
|
+
/** Le champ est-il invalide ? Pilote l'affichage des erreurs. Poussé par `[formField]`. @default false */
|
|
1470
|
+
invalid = input(false, /* @ts-ignore */
|
|
1471
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
1472
|
+
/** Champ obligatoire (marqueur visuel/ARIA). Poussé par `[formField]`. @default false */
|
|
1473
|
+
required = input(false, /* @ts-ignore */
|
|
1474
|
+
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
1475
|
+
/** Le champ a-t-il été modifié depuis sa valeur initiale ? Poussé par `[formField]`. @default false */
|
|
1476
|
+
dirty = input(false, /* @ts-ignore */
|
|
1477
|
+
...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
1478
|
+
/** Erreurs de validation à afficher. Poussé par `[formField]`. @default [] */
|
|
1479
|
+
errors = input([], /* @ts-ignore */
|
|
1480
|
+
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
1481
|
+
/** Attribut `name` logique du contrôle (à titre indicatif). @default '' */
|
|
1482
|
+
name = input('', /* @ts-ignore */
|
|
1483
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
1484
|
+
// --- Présentation ---
|
|
1485
|
+
/** id imposé (sélecteurs de test stables) ; sinon auto-généré par Field. @default undefined */
|
|
1486
|
+
id = input(/* @ts-ignore */
|
|
1487
|
+
...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
|
|
1488
|
+
/** Libellé du champ (rendu par Field). @default undefined */
|
|
1489
|
+
label = input(/* @ts-ignore */
|
|
1490
|
+
...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1491
|
+
/** Texte d'aide affiché sous le champ quand il est valide. @default undefined */
|
|
1492
|
+
hint = input(/* @ts-ignore */
|
|
1493
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
1494
|
+
/** Aide contextuelle riche : texte ou `TemplateRef` rendu dans une infobulle d'aide. @default undefined */
|
|
1495
|
+
helpText = input(/* @ts-ignore */
|
|
1496
|
+
...(ngDevMode ? [undefined, { debugName: "helpText" }] : /* istanbul ignore next */ []));
|
|
1497
|
+
/** Libellé accessible du bouton d'aide. @default 'Help' (ou KT_FIELD_CONFIG.helpLabel) */
|
|
1498
|
+
helpLabel = input(this.fieldConfig?.helpLabel ?? 'Help', /* @ts-ignore */
|
|
1499
|
+
...(ngDevMode ? [{ debugName: "helpLabel" }] : /* istanbul ignore next */ []));
|
|
1500
|
+
/** Force la valeur de `aria-describedby` (sinon dérivée de hint/erreur). @default undefined */
|
|
1501
|
+
customDescribedBy = input(/* @ts-ignore */
|
|
1502
|
+
...(ngDevMode ? [undefined, { debugName: "customDescribedBy" }] : /* istanbul ignore next */ []));
|
|
1503
|
+
/** Texte indicatif affiché quand aucune option n'est sélectionnée. @default undefined */
|
|
1504
|
+
placeholder = input(/* @ts-ignore */
|
|
1505
|
+
...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1506
|
+
/** Quand afficher l'erreur ; surcharge KT_FIELD_CONFIG et le défaut (`invalid && touched`). @default undefined */
|
|
1507
|
+
errorMatcher = input(/* @ts-ignore */
|
|
1508
|
+
...(ngDevMode ? [undefined, { debugName: "errorMatcher" }] : /* istanbul ignore next */ []));
|
|
1509
|
+
// --- Filtrage (champ de recherche dans le popup, opt-in) ---
|
|
1510
|
+
/** Affiche un champ de recherche en tête du popup (desktop) / de la sheet (téléphone).
|
|
1511
|
+
Pour les listes longues ; le texte tapé ne sert qu'à filtrer (réinitialisé à la fermeture). */
|
|
1512
|
+
filterable = input(false, /* @ts-ignore */
|
|
1513
|
+
...(ngDevMode ? [{ debugName: "filterable" }] : /* istanbul ignore next */ []));
|
|
1514
|
+
/** Placeholder du champ de recherche du popup. Défaut : `KT_SELECT_CONFIG.filterPlaceholder` ou vide. */
|
|
1515
|
+
filterPlaceholder = input(/* @ts-ignore */
|
|
1516
|
+
...(ngDevMode ? [undefined, { debugName: "filterPlaceholder" }] : /* istanbul ignore next */ []));
|
|
1517
|
+
/** Libellé accessible du champ de recherche. Défaut : `KT_SELECT_CONFIG.filterLabel` ou « Filter options ». */
|
|
1518
|
+
filterLabel = input(/* @ts-ignore */
|
|
1519
|
+
...(ngDevMode ? [undefined, { debugName: "filterLabel" }] : /* istanbul ignore next */ []));
|
|
1520
|
+
/** Prédicat de filtre custom `(option, texte tapé brut)`.
|
|
1521
|
+
Défaut : libellé de l'option, insensible à la casse et aux accents. */
|
|
1522
|
+
filterFn = input(/* @ts-ignore */
|
|
1523
|
+
...(ngDevMode ? [undefined, { debugName: "filterFn" }] : /* istanbul ignore next */ []));
|
|
1524
|
+
/** Nombre maximal d'options affichées dans le DOM en mode filtrable pour optimiser les performances. @default 100 */
|
|
1525
|
+
maxVisibleOptions = input(100, /* @ts-ignore */
|
|
1526
|
+
...(ngDevMode ? [{ debugName: "maxVisibleOptions" }] : /* istanbul ignore next */ []));
|
|
1527
|
+
/** Texte informatif de troncature des résultats (visuel). */
|
|
1528
|
+
truncatedResultsText = input(/* @ts-ignore */
|
|
1529
|
+
...(ngDevMode ? [undefined, { debugName: "truncatedResultsText" }] : /* istanbul ignore next */ []));
|
|
1530
|
+
/** Annonce de la troncature des résultats (live region). */
|
|
1531
|
+
truncatedResultsAnnouncement = input(/* @ts-ignore */
|
|
1532
|
+
...(ngDevMode ? [undefined, { debugName: "truncatedResultsAnnouncement" }] : /* istanbul ignore next */ []));
|
|
1533
|
+
// --- État interne du popup ---
|
|
1534
|
+
expanded = signal(false, /* @ts-ignore */
|
|
1535
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
1536
|
+
viewport = inject(KtViewport);
|
|
1537
|
+
/** Écran compact (téléphone / fenêtre étroite) : bascule le popup en bottom-sheet modale.
|
|
1538
|
+
Source unique configurable par appli (`KtViewport` / `provideKtBreakpoints`) ; pilote aussi
|
|
1539
|
+
la classe CSS `kt-select__popup--sheet` du template. */
|
|
1540
|
+
compact = this.viewport.isCompact;
|
|
1541
|
+
triggerEl = viewChild('trigger', /* @ts-ignore */
|
|
1542
|
+
...(ngDevMode ? [{ debugName: "triggerEl" }] : /* istanbul ignore next */ []));
|
|
1543
|
+
popupEl = viewChild('popup', /* @ts-ignore */
|
|
1544
|
+
...(ngDevMode ? [{ debugName: "popupEl" }] : /* istanbul ignore next */ []));
|
|
1545
|
+
idGen = inject(KtIdGenerator);
|
|
1546
|
+
uid = this.idGen.generateId('select');
|
|
1547
|
+
anchorName = `--kt-select-anchor-${this.uid}`;
|
|
1548
|
+
/** id du panneau (élément widget) en mode filtrable — cible de l'aria-controls du trigger. */
|
|
1549
|
+
panelId = `kt-select-panel-${this.uid}`;
|
|
1550
|
+
filterInputEl = viewChild('filterInput', /* @ts-ignore */
|
|
1551
|
+
...(ngDevMode ? [{ debugName: "filterInputEl" }] : /* istanbul ignore next */ []));
|
|
1552
|
+
listboxEl = viewChild('listboxEl', /* @ts-ignore */
|
|
1553
|
+
...(ngDevMode ? [{ debugName: "listboxEl" }] : /* istanbul ignore next */ []));
|
|
1554
|
+
listboxDir = viewChild(Listbox, /* @ts-ignore */
|
|
1555
|
+
...(ngDevMode ? [{ debugName: "listboxDir" }] : /* istanbul ignore next */ []));
|
|
1556
|
+
comboboxDir = viewChild(Combobox, /* @ts-ignore */
|
|
1557
|
+
...(ngDevMode ? [{ debugName: "comboboxDir" }] : /* istanbul ignore next */ []));
|
|
1558
|
+
/** Suit l'état réel du Popover : la fermeture peut être synchrone (Tab) ET pilotée par
|
|
1559
|
+
l'effect — sans ce flag, le second `hidePopover()` jetterait une InvalidStateError. */
|
|
1560
|
+
popoverShown = false;
|
|
1561
|
+
// --- Politique d'affichage des erreurs (cohérente avec les champs) ---
|
|
1562
|
+
matcher = computed(() => this.errorMatcher() ?? this.fieldConfig?.errorMatcher ?? defaultKtFieldErrorMatcher, /* @ts-ignore */
|
|
1563
|
+
...(ngDevMode ? [{ debugName: "matcher" }] : /* istanbul ignore next */ []));
|
|
1564
|
+
showInvalid = computed(() => this.matcher()({ invalid: this.invalid(), touched: this.touched(), dirty: this.dirty() }), /* @ts-ignore */
|
|
1565
|
+
...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
|
|
1566
|
+
// --- Textes résolus (input > KT_SELECT_CONFIG > défaut lib neutre EN) ---
|
|
1567
|
+
resolvedPlaceholder = computed(() => this.placeholder() ?? this.config?.placeholder ?? '', /* @ts-ignore */
|
|
1568
|
+
...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
1569
|
+
resolvedEmptyText = computed(() => this.config?.emptyText ?? 'No options', /* @ts-ignore */
|
|
1570
|
+
...(ngDevMode ? [{ debugName: "resolvedEmptyText" }] : /* istanbul ignore next */ []));
|
|
1571
|
+
resolvedCloseLabel = computed(() => this.config?.closeLabel ?? 'Close', /* @ts-ignore */
|
|
1572
|
+
...(ngDevMode ? [{ debugName: "resolvedCloseLabel" }] : /* istanbul ignore next */ []));
|
|
1573
|
+
resolvedFilterPlaceholder = computed(() => this.filterPlaceholder() ?? this.config?.filterPlaceholder ?? '', /* @ts-ignore */
|
|
1574
|
+
...(ngDevMode ? [{ debugName: "resolvedFilterPlaceholder" }] : /* istanbul ignore next */ []));
|
|
1575
|
+
resolvedFilterLabel = computed(() => this.filterLabel() ?? this.config?.filterLabel ?? 'Filter options', /* @ts-ignore */
|
|
1576
|
+
...(ngDevMode ? [{ debugName: "resolvedFilterLabel" }] : /* istanbul ignore next */ []));
|
|
1577
|
+
resolvedFilterResultsText = computed(() => this.config?.filterResultsText ?? defaultFilterResultsText, /* @ts-ignore */
|
|
1578
|
+
...(ngDevMode ? [{ debugName: "resolvedFilterResultsText" }] : /* istanbul ignore next */ []));
|
|
1579
|
+
resolvedTruncatedResultsText = computed(() => this.truncatedResultsText() ?? this.config?.truncatedResultsText ?? defaultTruncatedResultsText, /* @ts-ignore */
|
|
1580
|
+
...(ngDevMode ? [{ debugName: "resolvedTruncatedResultsText" }] : /* istanbul ignore next */ []));
|
|
1581
|
+
resolvedTruncatedResultsAnnouncement = computed(() => this.truncatedResultsAnnouncement() ??
|
|
1582
|
+
this.config?.truncatedResultsAnnouncement ??
|
|
1583
|
+
defaultTruncatedResultsAnnouncement, /* @ts-ignore */
|
|
1584
|
+
...(ngDevMode ? [{ debugName: "resolvedTruncatedResultsAnnouncement" }] : /* istanbul ignore next */ []));
|
|
1585
|
+
// Field attend des { kind, message } ; on mappe les ValidationError.
|
|
1586
|
+
fieldErrors = computed(() => this.errors().map((e) => ({ kind: e.kind, message: e.message })), /* @ts-ignore */
|
|
1587
|
+
...(ngDevMode ? [{ debugName: "fieldErrors" }] : /* istanbul ignore next */ []));
|
|
1588
|
+
// --- Accès aux options ---
|
|
1589
|
+
labelAccessor = computed(() => accessor(this.optionLabel(), defaultLabel), /* @ts-ignore */
|
|
1590
|
+
...(ngDevMode ? [{ debugName: "labelAccessor" }] : /* istanbul ignore next */ []));
|
|
1591
|
+
valueAccessor = computed(() => accessor(this.optionValue(), defaultIdentity), /* @ts-ignore */
|
|
1592
|
+
...(ngDevMode ? [{ debugName: "valueAccessor" }] : /* istanbul ignore next */ []));
|
|
1593
|
+
disabledAccessor = computed(() => accessor(this.optionDisabled(), () => false), /* @ts-ignore */
|
|
1594
|
+
...(ngDevMode ? [{ debugName: "disabledAccessor" }] : /* istanbul ignore next */ []));
|
|
1595
|
+
/** Comparateur effectif en mode objet : `compareWith`, sinon égalité par identité. */
|
|
1596
|
+
comparator = computed(() => this.compareWith() ?? ((a, b) => this.keyOf(a) === this.keyOf(b)), /* @ts-ignore */
|
|
1597
|
+
...(ngDevMode ? [{ debugName: "comparator" }] : /* istanbul ignore next */ []));
|
|
1598
|
+
labelOf(item) {
|
|
1599
|
+
return this.labelAccessor()(item);
|
|
1600
|
+
}
|
|
1601
|
+
keyOf(item) {
|
|
1602
|
+
return this.valueAccessor()(item);
|
|
1603
|
+
}
|
|
1604
|
+
disabledOf(item) {
|
|
1605
|
+
return this.disabledAccessor()(item);
|
|
1606
|
+
}
|
|
1607
|
+
// --- Filtrage : état éphémère (réinitialisé à la fermeture du popup) ---
|
|
1608
|
+
filterText = signal('', /* @ts-ignore */
|
|
1609
|
+
...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
|
|
1610
|
+
/** Annonce différée du nombre de résultats (live region `role="status"`). */
|
|
1611
|
+
announcedCount = signal('', /* @ts-ignore */
|
|
1612
|
+
...(ngDevMode ? [{ debugName: "announcedCount" }] : /* istanbul ignore next */ []));
|
|
1613
|
+
announceTimer;
|
|
1614
|
+
/** Options visibles : les options, restreintes par le filtre quand il est actif. */
|
|
1615
|
+
filteredOptions = computed(() => {
|
|
1616
|
+
const query = this.filterText().trim();
|
|
1617
|
+
if (!this.filterable() || query === '')
|
|
1618
|
+
return this.options();
|
|
1619
|
+
const fn = this.filterFn() ??
|
|
1620
|
+
((option, q) => normalizeForFilter(this.labelOf(option)).includes(normalizeForFilter(q)));
|
|
1621
|
+
return this.options().filter((option) => fn(option, query));
|
|
1622
|
+
}, /* @ts-ignore */
|
|
1623
|
+
...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
|
|
1624
|
+
/** Les options affichées dans le DOM. La limitation (maxVisibleOptions) n'est appliquée qu'en mode filtrable. */
|
|
1625
|
+
displayedOptions = computed(() => {
|
|
1626
|
+
const opts = this.filteredOptions();
|
|
1627
|
+
if (!this.filterable())
|
|
1628
|
+
return opts;
|
|
1629
|
+
const limit = this.maxVisibleOptions();
|
|
1630
|
+
return opts.length <= limit ? opts : opts.slice(0, limit);
|
|
1631
|
+
}, /* @ts-ignore */
|
|
1632
|
+
...(ngDevMode ? [{ debugName: "displayedOptions" }] : /* istanbul ignore next */ []));
|
|
1633
|
+
/** Texte de la live region du filtre. Virtuel : le multi y ajoute « , M selected ». */
|
|
1634
|
+
composeFilterAnnouncement() {
|
|
1635
|
+
const query = this.filterText().trim();
|
|
1636
|
+
if (query === '')
|
|
1637
|
+
return '';
|
|
1638
|
+
const total = this.filteredOptions().length;
|
|
1639
|
+
const limit = this.maxVisibleOptions();
|
|
1640
|
+
if (this.filterable() && total > limit) {
|
|
1641
|
+
return this.resolvedTruncatedResultsAnnouncement()(limit, total);
|
|
1642
|
+
}
|
|
1643
|
+
return this.resolvedFilterResultsText()(total);
|
|
1644
|
+
}
|
|
1645
|
+
constructor() {
|
|
1646
|
+
// Ancre le trigger : `setProperty` natif (les propriétés CSS à tiret comme `anchor-name`
|
|
1647
|
+
// ne sont pas appliquées de façon fiable par le binding `[style.…]`/Renderer2 — cf. Tooltip).
|
|
1648
|
+
effect(() => {
|
|
1649
|
+
this.triggerEl()?.nativeElement.style.setProperty('anchor-name', this.anchorName);
|
|
1650
|
+
});
|
|
1651
|
+
// Affichage du popup via Popover natif (top-layer). On NE peut PAS utiliser <dialog>.showModal()
|
|
1652
|
+
// (vraie modale) : ce combobox @angular/aria se ferme au blur (closePopupOnBlur) et showModal
|
|
1653
|
+
// déplace le focus hors du combobox → fermeture immédiate. On reste donc en Popover (le focus
|
|
1654
|
+
// reste sur le combobox) ; sur mobile le focusMode 'roving' rend les options réellement focusables.
|
|
1655
|
+
effect(() => {
|
|
1656
|
+
const el = this.popupEl()?.nativeElement;
|
|
1657
|
+
if (!el)
|
|
1658
|
+
return;
|
|
1659
|
+
if (this.expanded()) {
|
|
1660
|
+
el.classList.remove('kt-select__popup--closing');
|
|
1661
|
+
el.style.translate = ''; // efface un éventuel translate résiduel d'un drag-to-dismiss précédent
|
|
1662
|
+
el.style.setProperty('position-anchor', this.anchorName);
|
|
1663
|
+
el.showPopover?.();
|
|
1664
|
+
this.popoverShown = true;
|
|
1665
|
+
// Desktop filtrable : le champ de recherche prend le focus à l'ouverture (pattern
|
|
1666
|
+
// SelectPanel). Pas sur téléphone : le clavier virtuel recouvrirait la bottom-sheet.
|
|
1667
|
+
if (this.filterable() && !this.compact())
|
|
1668
|
+
this.filterInputEl()?.nativeElement.focus();
|
|
1669
|
+
}
|
|
1670
|
+
else if (this.popoverShown) {
|
|
1671
|
+
this.animateAndCloseSelect(el);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
// Filtre éphémère : réinitialisé à la fermeture (liste complète à la réouverture).
|
|
1675
|
+
effect(() => {
|
|
1676
|
+
if (this.expanded())
|
|
1677
|
+
return;
|
|
1678
|
+
untracked(() => {
|
|
1679
|
+
this.filterText.set('');
|
|
1680
|
+
this.announcedCount.set('');
|
|
1681
|
+
clearTimeout(this.announceTimer);
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
this.destroyRef.onDestroy(() => {
|
|
1685
|
+
clearTimeout(this.announceTimer);
|
|
1686
|
+
this.sheetDrag.destroy();
|
|
1687
|
+
if (isPlatformBrowser(this.platformId) && this.compact() && this.expanded()) {
|
|
1688
|
+
this.doc.body.style.overflow = '';
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
// Préserve le contenu du template sur mobile pour permettre l'animation de sortie
|
|
1692
|
+
effect(() => {
|
|
1693
|
+
const cb = this.comboboxDir();
|
|
1694
|
+
if (cb) {
|
|
1695
|
+
cb.preserveContent.set(this.compact());
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
// Sheet mobile : verrouille le scroll du fond et restitue le focus au trigger à la fermeture.
|
|
1699
|
+
effect(() => {
|
|
1700
|
+
if (!this.compact())
|
|
1701
|
+
return;
|
|
1702
|
+
if (!isPlatformBrowser(this.platformId))
|
|
1703
|
+
return;
|
|
1704
|
+
if (this.expanded()) {
|
|
1705
|
+
this.doc.body.style.overflow = 'hidden';
|
|
1706
|
+
this.wasExpanded = true;
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
this.doc.body.style.overflow = '';
|
|
1710
|
+
if (this.wasExpanded) {
|
|
1711
|
+
this.triggerEl()?.nativeElement.focus();
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
// Ferme au clic/tap en dehors du composant (remplace le binding d'hôte statique document:pointerdown)
|
|
1716
|
+
// Enregistré une seule fois hors de la zone Angular pour éviter le change detection global sur chaque clic.
|
|
1717
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1718
|
+
this.ngZone.runOutsideAngular(() => {
|
|
1719
|
+
const onPointerDown = (event) => {
|
|
1720
|
+
if (!this.expanded())
|
|
1721
|
+
return;
|
|
1722
|
+
const target = event.target;
|
|
1723
|
+
const popup = this.popupEl()?.nativeElement;
|
|
1724
|
+
if (target && (!this.host.nativeElement.contains(target) || target === popup)) {
|
|
1725
|
+
this.ngZone.run(() => {
|
|
1726
|
+
this.expanded.set(false);
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
this.doc.addEventListener('pointerdown', onPointerDown);
|
|
1731
|
+
this.destroyRef.onDestroy(() => {
|
|
1732
|
+
this.doc.removeEventListener('pointerdown', onPointerDown);
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
// Fait défiler la liste pour afficher l'option active lors de la navigation au clavier (mode activedescendant).
|
|
1737
|
+
effect(() => {
|
|
1738
|
+
const activeId = this.listboxDir()?.activeDescendant();
|
|
1739
|
+
if (!this.expanded() || !activeId)
|
|
1740
|
+
return;
|
|
1741
|
+
requestAnimationFrame(() => {
|
|
1742
|
+
const listbox = this.listboxEl()?.nativeElement;
|
|
1743
|
+
if (!listbox)
|
|
1744
|
+
return;
|
|
1745
|
+
const activeEl = listbox.querySelector('.kt-select__option--active');
|
|
1746
|
+
if (activeEl) {
|
|
1747
|
+
activeEl.scrollIntoView({ block: 'nearest' });
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
if (isDevMode()) {
|
|
1752
|
+
effect(() => {
|
|
1753
|
+
const opts = this.options();
|
|
1754
|
+
if (this.optionValue() !== undefined || this.compareWith() !== undefined)
|
|
1755
|
+
return;
|
|
1756
|
+
const sample = opts.find((o) => o !== null && typeof o === 'object');
|
|
1757
|
+
if (sample && sample['id'] === undefined && sample['value'] === undefined) {
|
|
1758
|
+
console.warn(`[${this.host.nativeElement.tagName.toLowerCase()}] Options de type objet sans identité résoluble ` +
|
|
1759
|
+
"('id'/'value') ni `compareWith` : la sélection peut être incohérente au rechargement " +
|
|
1760
|
+
'des données. Fournissez `optionValue` ou `compareWith`.');
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
onFilterInput(event) {
|
|
1766
|
+
this.filterText.set(event.target.value);
|
|
1767
|
+
this.scheduleResultsAnnouncement();
|
|
1768
|
+
this.resyncListboxSelection();
|
|
1769
|
+
}
|
|
1770
|
+
/** Relaye la navigation vers le listbox : le focus DOM est dans le champ de filtre et les
|
|
1771
|
+
keydown du popup ne remontent pas au combobox (le popup est un sibling DOM du trigger).
|
|
1772
|
+
Shift+flèches : extension de plage (multi ; sans effet en single). Home/End/Espace/
|
|
1773
|
+
caractères restent dans le champ (convention APG des combobox éditables). */
|
|
1774
|
+
onFilterKeydown(event) {
|
|
1775
|
+
if (event.key === 'Escape') {
|
|
1776
|
+
event.preventDefault();
|
|
1777
|
+
event.stopPropagation();
|
|
1778
|
+
this.expanded.set(false);
|
|
1779
|
+
this.refocusTriggerAfterFilterClose();
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (event.key === 'Tab') {
|
|
1783
|
+
// APG : Tab ferme le popup et suit l'ordre de tabulation. Fermeture SYNCHRONE (l'effect
|
|
1784
|
+
// est différé : le popup, sibling DOM du trigger, resterait tabbable pendant l'action
|
|
1785
|
+
// par défaut) + focus sur le trigger SANS preventDefault : le Tab natif repart du
|
|
1786
|
+
// trigger vers l'élément suivant de la page (Shift+Tab : le précédent).
|
|
1787
|
+
this.closePopupNow();
|
|
1788
|
+
this.refocusTriggerAfterFilterClose();
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter')
|
|
1792
|
+
return;
|
|
1793
|
+
const listbox = this.listboxEl()?.nativeElement;
|
|
1794
|
+
if (!listbox)
|
|
1795
|
+
return;
|
|
1796
|
+
event.preventDefault();
|
|
1797
|
+
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: event.key, shiftKey: event.shiftKey, cancelable: true }));
|
|
1798
|
+
}
|
|
1799
|
+
/** Échap depuis n'importe où dans le composant popup ouvert : après un clic souris sur une
|
|
1800
|
+
option, le focus DOM est sur le listbox (pas le trigger) — ni le listbox ni le combobox ne
|
|
1801
|
+
gèrent Échap là. Le popup est rendu dans le sous-arbre DOM de l'hôte : l'événement y bulle.
|
|
1802
|
+
(Trigger et champ de filtre gèrent leur Échap en amont avec stopPropagation : pas de doublon.) */
|
|
1803
|
+
onHostKeydown(event) {
|
|
1804
|
+
if (event.key !== 'Escape' || !this.expanded())
|
|
1805
|
+
return;
|
|
1806
|
+
event.preventDefault();
|
|
1807
|
+
event.stopPropagation();
|
|
1808
|
+
this.expanded.set(false);
|
|
1809
|
+
this.triggerEl()?.nativeElement.focus();
|
|
1810
|
+
}
|
|
1811
|
+
/** Annonce le contenu du filtre ~500 ms après la dernière frappe (évite le spam SR). */
|
|
1812
|
+
scheduleResultsAnnouncement() {
|
|
1813
|
+
this.ngZone.runOutsideAngular(() => {
|
|
1814
|
+
clearTimeout(this.announceTimer);
|
|
1815
|
+
this.announceTimer = setTimeout(() => {
|
|
1816
|
+
this.ngZone.run(() => {
|
|
1817
|
+
this.announcedCount.set(this.composeFilterAnnouncement());
|
|
1818
|
+
});
|
|
1819
|
+
}, ANNOUNCE_DELAY_MS);
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
/** Annonce immédiate dans la live region du panneau (annule une annonce différée en attente). */
|
|
1823
|
+
announceNow(text) {
|
|
1824
|
+
clearTimeout(this.announceTimer);
|
|
1825
|
+
this.announcedCount.set(text);
|
|
1826
|
+
}
|
|
1827
|
+
/** Séquelle de la purge interne du Listbox (il vide sa valeur dès qu'un item n'est plus
|
|
1828
|
+
rendu) : quand le filtre redécouvre des options sélectionnées, le binding `[value]` ne
|
|
1829
|
+
re-pousse rien (`listboxValue` n'a pas changé) — on resynchronise le modèle du Listbox
|
|
1830
|
+
avec les clés sélectionnées actuellement visibles. */
|
|
1831
|
+
resyncListboxSelection() {
|
|
1832
|
+
const listbox = this.listboxDir();
|
|
1833
|
+
if (!listbox)
|
|
1834
|
+
return;
|
|
1835
|
+
const visibleKeys = new Set(this.filteredOptions().map((o) => this.keyOf(o)));
|
|
1836
|
+
const expected = this.listboxValue().filter((k) => visibleKeys.has(k));
|
|
1837
|
+
const actual = listbox.value();
|
|
1838
|
+
if (expected.length === actual.length && expected.every((k) => actual.includes(k)))
|
|
1839
|
+
return;
|
|
1840
|
+
listbox.value.set(expected);
|
|
1841
|
+
}
|
|
1842
|
+
/** Desktop filtrable : le focus DOM vit dans le popup (champ/options) — on le rend au trigger
|
|
1843
|
+
à la fermeture, sinon il tombe sur `<body>`. (Écran compact : déjà géré par l'effect `compact`.) */
|
|
1844
|
+
refocusTriggerAfterFilterClose() {
|
|
1845
|
+
if (!this.filterable() || this.compact())
|
|
1846
|
+
return;
|
|
1847
|
+
this.triggerEl()?.nativeElement.focus();
|
|
1848
|
+
}
|
|
1849
|
+
/** Fermeture synchrone du Popover (l'effect, différé, ne re-cachera pas : flag popoverShown). */
|
|
1850
|
+
closePopupNow() {
|
|
1851
|
+
this.expanded.set(false);
|
|
1852
|
+
if (!this.popoverShown)
|
|
1853
|
+
return;
|
|
1854
|
+
const el = this.popupEl()?.nativeElement;
|
|
1855
|
+
el?.classList.remove('kt-select__popup--closing');
|
|
1856
|
+
el?.hidePopover?.();
|
|
1857
|
+
this.popoverShown = false;
|
|
1858
|
+
}
|
|
1859
|
+
// --- Drag-to-dismiss de la bottom-sheet (téléphone) ---
|
|
1860
|
+
// Geste DOUBLÉ par le bouton Fermer + tap-extérieur + Échap (WCAG 2.5.1). Logique FACTORISÉE
|
|
1861
|
+
// avec le mode `sheet` du Dialog via createKtSheetDrag (@ktortu/aaa).
|
|
1862
|
+
sheetDrag = createKtSheetDrag({
|
|
1863
|
+
pane: () => this.popupEl()?.nativeElement.querySelector('.kt-select__sheet-card') ?? null,
|
|
1864
|
+
onDismiss: () => this.expanded.set(false),
|
|
1865
|
+
draggingClass: 'kt-select__popup--dragging',
|
|
1866
|
+
});
|
|
1867
|
+
onDragStart(event) {
|
|
1868
|
+
if (!this.compact())
|
|
1869
|
+
return; // glissement actif sur écran compact uniquement
|
|
1870
|
+
this.sheetDrag.start(event);
|
|
1871
|
+
}
|
|
1872
|
+
animateAndCloseSelect(el) {
|
|
1873
|
+
if (!this.compact() || !isPlatformBrowser(this.platformId)) {
|
|
1874
|
+
el.hidePopover?.();
|
|
1875
|
+
this.popoverShown = false;
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
el.classList.add('kt-select__popup--closing');
|
|
1879
|
+
const card = el.querySelector('.kt-select__sheet-card') || el;
|
|
1880
|
+
// Récupérer la durée de transition configurée en CSS (ex: "150ms" ou "0.2s")
|
|
1881
|
+
const styles = window.getComputedStyle(card);
|
|
1882
|
+
const durationStr = styles.transitionDuration || '0s';
|
|
1883
|
+
const durationMs = parseFloat(durationStr) * (durationStr.includes('ms') ? 1 : 1000);
|
|
1884
|
+
if (durationMs === 0) {
|
|
1885
|
+
el.hidePopover?.();
|
|
1886
|
+
el.classList.remove('kt-select__popup--closing');
|
|
1887
|
+
this.popoverShown = false;
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
// Écouter la fin de la transition sur la propriété 'translate'
|
|
1891
|
+
const onTransitionEnd = (event) => {
|
|
1892
|
+
if (event.target === card && event.propertyName === 'translate') {
|
|
1893
|
+
card.removeEventListener('transitionend', onTransitionEnd);
|
|
1894
|
+
if (this.expanded())
|
|
1895
|
+
return;
|
|
1896
|
+
el.hidePopover?.();
|
|
1897
|
+
el.classList.remove('kt-select__popup--closing');
|
|
1898
|
+
this.popoverShown = false;
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
card.addEventListener('transitionend', onTransitionEnd);
|
|
1902
|
+
// Sécurité (au cas où la transition n'aboutirait pas)
|
|
1903
|
+
setTimeout(() => {
|
|
1904
|
+
card.removeEventListener('transitionend', onTransitionEnd);
|
|
1905
|
+
if (this.expanded())
|
|
1906
|
+
return;
|
|
1907
|
+
if (this.popoverShown) {
|
|
1908
|
+
el.hidePopover?.();
|
|
1909
|
+
el.classList.remove('kt-select__popup--closing');
|
|
1910
|
+
this.popoverShown = false;
|
|
1911
|
+
}
|
|
1912
|
+
}, durationMs + 50);
|
|
1913
|
+
}
|
|
1914
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1915
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.1", type: KtBaseSelect, isStandalone: true, inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, helpText: { classPropertyName: "helpText", publicName: "helpText", isSignal: true, isRequired: false, transformFunction: null }, helpLabel: { classPropertyName: "helpLabel", publicName: "helpLabel", isSignal: true, isRequired: false, transformFunction: null }, customDescribedBy: { classPropertyName: "customDescribedBy", publicName: "customDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, errorMatcher: { classPropertyName: "errorMatcher", publicName: "errorMatcher", isSignal: true, isRequired: false, transformFunction: null }, filterable: { classPropertyName: "filterable", publicName: "filterable", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, filterLabel: { classPropertyName: "filterLabel", publicName: "filterLabel", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleOptions: { classPropertyName: "maxVisibleOptions", publicName: "maxVisibleOptions", isSignal: true, isRequired: false, transformFunction: null }, truncatedResultsText: { classPropertyName: "truncatedResultsText", publicName: "truncatedResultsText", isSignal: true, isRequired: false, transformFunction: null }, truncatedResultsAnnouncement: { classPropertyName: "truncatedResultsAnnouncement", publicName: "truncatedResultsAnnouncement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { helpClick: "helpClick", touched: "touchedChange" }, host: { listeners: { "keydown": "onHostKeydown($event)" } }, viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "popupEl", first: true, predicate: ["popup"], descendants: true, isSignal: true }, { propertyName: "filterInputEl", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }, { propertyName: "listboxEl", first: true, predicate: ["listboxEl"], descendants: true, isSignal: true }, { propertyName: "listboxDir", first: true, predicate: Listbox, descendants: true, isSignal: true }, { propertyName: "comboboxDir", first: true, predicate: Combobox, descendants: true, isSignal: true }], ngImport: i0 });
|
|
1916
|
+
}
|
|
1917
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseSelect, decorators: [{
|
|
1918
|
+
type: Directive,
|
|
1919
|
+
args: [{
|
|
1920
|
+
host: {
|
|
1921
|
+
'(keydown)': 'onHostKeydown($event)',
|
|
1922
|
+
},
|
|
1923
|
+
}]
|
|
1924
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], helpClick: [{ type: i0.Output, args: ["helpClick"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], helpText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpText", required: false }] }], helpLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpLabel", required: false }] }], customDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "customDescribedBy", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], errorMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMatcher", required: false }] }], filterable: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterable", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], filterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterLabel", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], maxVisibleOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleOptions", required: false }] }], truncatedResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "truncatedResultsText", required: false }] }], truncatedResultsAnnouncement: [{ type: i0.Input, args: [{ isSignal: true, alias: "truncatedResultsAnnouncement", required: false }] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], popupEl: [{ type: i0.ViewChild, args: ['popup', { isSignal: true }] }], filterInputEl: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }], listboxEl: [{ type: i0.ViewChild, args: ['listboxEl', { isSignal: true }] }], listboxDir: [{ type: i0.ViewChild, args: [i0.forwardRef(() => Listbox), { isSignal: true }] }], comboboxDir: [{ type: i0.ViewChild, args: [i0.forwardRef(() => Combobox), { isSignal: true }] }] } });
|
|
1925
|
+
|
|
1926
|
+
/** Template de rendu d'une option, posé sur un `<ng-template>` projeté dans `kt-select`.
|
|
1927
|
+
L'input sert UNIQUEMENT à inférer `T` (re-bind de la liste d'options) ; `ngTemplateContextGuard`
|
|
1928
|
+
type alors les variables `let-` (`option: T`, `selected`/`active: boolean`). Requiert `strictTemplates`.
|
|
1929
|
+
|
|
1930
|
+
```html
|
|
1931
|
+
<kt-select [options]="users" optionLabel="name">
|
|
1932
|
+
<ng-template [ktSelectOption]="users" let-user let-selected="selected">
|
|
1933
|
+
<user-badge [user]="user" /> @if (selected) { ✓ }
|
|
1934
|
+
</ng-template>
|
|
1935
|
+
</kt-select>
|
|
1936
|
+
``` */
|
|
1937
|
+
class KtSelectOptionDef {
|
|
1938
|
+
ktSelectOption = input.required(/* @ts-ignore */
|
|
1939
|
+
...(ngDevMode ? [{ debugName: "ktSelectOption" }] : /* istanbul ignore next */ []));
|
|
1940
|
+
template = inject(TemplateRef);
|
|
1941
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1942
|
+
return typeof ctx === 'object';
|
|
1943
|
+
}
|
|
1944
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectOptionDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1945
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtSelectOptionDef, isStandalone: true, selector: "ng-template[ktSelectOption]", inputs: { ktSelectOption: { classPropertyName: "ktSelectOption", publicName: "ktSelectOption", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
1946
|
+
}
|
|
1947
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectOptionDef, decorators: [{
|
|
1948
|
+
type: Directive,
|
|
1949
|
+
args: [{ selector: 'ng-template[ktSelectOption]' }]
|
|
1950
|
+
}], propDecorators: { ktSelectOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktSelectOption", required: true }] }] } });
|
|
1951
|
+
/** Template de rendu du trigger (libellé de la valeur courante). Même mécanisme typé que `ktSelectOption`.
|
|
1952
|
+
Contexte : `let-option` = l'option sélectionnée (`T | null`).
|
|
1953
|
+
|
|
1954
|
+
```html
|
|
1955
|
+
<kt-select [options]="users" [(value)]="selected">
|
|
1956
|
+
<ng-template [ktSelectTrigger]="users" let-user>
|
|
1957
|
+
@if (user) { <user-badge [user]="user" /> } @else { Choisir… }
|
|
1958
|
+
</ng-template>
|
|
1959
|
+
</kt-select>
|
|
1960
|
+
``` */
|
|
1961
|
+
class KtSelectTriggerDef {
|
|
1962
|
+
ktSelectTrigger = input.required(/* @ts-ignore */
|
|
1963
|
+
...(ngDevMode ? [{ debugName: "ktSelectTrigger" }] : /* istanbul ignore next */ []));
|
|
1964
|
+
template = inject(TemplateRef);
|
|
1965
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1966
|
+
return typeof ctx === 'object';
|
|
1967
|
+
}
|
|
1968
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectTriggerDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1969
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtSelectTriggerDef, isStandalone: true, selector: "ng-template[ktSelectTrigger]", inputs: { ktSelectTrigger: { classPropertyName: "ktSelectTrigger", publicName: "ktSelectTrigger", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
1970
|
+
}
|
|
1971
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectTriggerDef, decorators: [{
|
|
1972
|
+
type: Directive,
|
|
1973
|
+
args: [{ selector: 'ng-template[ktSelectTrigger]' }]
|
|
1974
|
+
}], propDecorators: { ktSelectTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktSelectTrigger", required: true }] }] } });
|
|
1975
|
+
|
|
1976
|
+
/** Select single (« select-only combobox » WAI-ARIA) bâti sur `@angular/aria`.
|
|
1977
|
+
Choix dans une liste fermée, sans saisie. Présentation responsive (un seul Popover, CSS @media) :
|
|
1978
|
+
desktop = dropdown ancré (focus sur le trigger / activedescendant) ; téléphone tactile = bottom-sheet
|
|
1979
|
+
en bas d'écran (focusMode 'roving', options réellement focusables, scrim ::backdrop, cibles ≥44px).
|
|
1980
|
+
Composé avec `Field` ; s'intègre aux Signal Forms via `[formField]`.
|
|
1981
|
+
Tout le commun (popup, filtre, clavier, drag-to-dismiss) vit dans `BaseSelect`.
|
|
1982
|
+
|
|
1983
|
+
@template T Type d'une option (élément du tableau `options`).
|
|
1984
|
+
@template V Type de la valeur émise (défaut `T` ; clé extraite si `optionValue` est fourni).
|
|
1985
|
+
|
|
1986
|
+
@example
|
|
1987
|
+
```html
|
|
1988
|
+
<kt-select [options]="users" optionLabel="name" optionValue="id" [(value)]="selectedId" />
|
|
1989
|
+
``` */
|
|
1990
|
+
class KtSelect extends KtBaseSelect {
|
|
1991
|
+
// --- Modèle / sortie ---
|
|
1992
|
+
/** Valeur sélectionnée (clé extraite, ou objet entier si `optionValue` est omis ; `null` si rien). @default null */
|
|
1993
|
+
value = model(null, /* @ts-ignore */
|
|
1994
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1995
|
+
/** Émis à chaque sélection. Payload : `{ value, option }` — `value` est la valeur émise (clé ou objet),
|
|
1996
|
+
`option` l'objet option correspondant (`null` si la sélection est effacée). */
|
|
1997
|
+
selectionChange = output();
|
|
1998
|
+
// --- Templates projetés (typés via ngTemplateContextGuard) ---
|
|
1999
|
+
optionDef = contentChild(KtSelectOptionDef, /* @ts-ignore */
|
|
2000
|
+
...(ngDevMode ? [{ debugName: "optionDef" }] : /* istanbul ignore next */ []));
|
|
2001
|
+
triggerDef = contentChild(KtSelectTriggerDef, /* @ts-ignore */
|
|
2002
|
+
...(ngDevMode ? [{ debugName: "triggerDef" }] : /* istanbul ignore next */ []));
|
|
2003
|
+
/** Fermer le popup après une sélection (sémantique single ; le multi reste ouvert). */
|
|
2004
|
+
closeOnSelect = computed(() => this.config?.closeOnSelect ?? true, /* @ts-ignore */
|
|
2005
|
+
...(ngDevMode ? [{ debugName: "closeOnSelect" }] : /* istanbul ignore next */ []));
|
|
2006
|
+
/** Option correspondant à la valeur courante (clé extraite, ou objet si `optionValue` est omis). */
|
|
2007
|
+
selectedOption = computed(() => {
|
|
2008
|
+
const v = this.value();
|
|
2009
|
+
if (v === null || v === undefined)
|
|
2010
|
+
return null;
|
|
2011
|
+
const opts = this.options();
|
|
2012
|
+
if (this.optionValue() !== undefined) {
|
|
2013
|
+
return opts.find((o) => this.keyOf(o) === v) ?? null;
|
|
2014
|
+
}
|
|
2015
|
+
const cmp = this.comparator();
|
|
2016
|
+
return opts.find((o) => cmp(o, v)) ?? v;
|
|
2017
|
+
}, /* @ts-ignore */
|
|
2018
|
+
...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
|
|
2019
|
+
/** Valeur du listbox : tableau de clés (0 ou 1 élément en single). */
|
|
2020
|
+
listboxValue = computed(() => {
|
|
2021
|
+
const opt = this.selectedOption();
|
|
2022
|
+
return opt === null ? [] : [this.keyOf(opt)];
|
|
2023
|
+
}, /* @ts-ignore */
|
|
2024
|
+
...(ngDevMode ? [{ debugName: "listboxValue" }] : /* istanbul ignore next */ []));
|
|
2025
|
+
onListboxValueChange(keys) {
|
|
2026
|
+
// Purge interne du Listbox quand le filtre masque l'option sélectionnée (la lib vide sa
|
|
2027
|
+
// valeur dès que l'item n'est plus rendu) : pas un geste utilisateur, on l'ignore.
|
|
2028
|
+
if (keys.length === 0 && this.isSelectionFilteredOut())
|
|
2029
|
+
return;
|
|
2030
|
+
const key = keys.length > 0 ? keys[0] : null;
|
|
2031
|
+
if (key === null || key === undefined) {
|
|
2032
|
+
this.commitSelection(null);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const option = this.options().find((o) => this.keyOf(o) === key) ?? null;
|
|
2036
|
+
this.commitSelection(option);
|
|
2037
|
+
}
|
|
2038
|
+
/** La sélection courante est-elle masquée par le filtre ou la troncation ? (≠ retirée des données). */
|
|
2039
|
+
isSelectionFilteredOut() {
|
|
2040
|
+
if (!this.filterable())
|
|
2041
|
+
return false;
|
|
2042
|
+
const selected = this.selectedOption();
|
|
2043
|
+
if (selected === null)
|
|
2044
|
+
return false;
|
|
2045
|
+
const key = this.keyOf(selected);
|
|
2046
|
+
return !this.displayedOptions().some((option) => this.keyOf(option) === key);
|
|
2047
|
+
}
|
|
2048
|
+
commitSelection(option) {
|
|
2049
|
+
const out = option === null ? null : this.optionValue() !== undefined ? this.keyOf(option) : option;
|
|
2050
|
+
this.value.set(out);
|
|
2051
|
+
this.touched.set(true);
|
|
2052
|
+
this.selectionChange.emit({ value: out, option });
|
|
2053
|
+
if (this.closeOnSelect()) {
|
|
2054
|
+
this.expanded.set(false);
|
|
2055
|
+
this.refocusTriggerAfterFilterClose();
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelect, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
2059
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtSelect, isStandalone: true, selector: "kt-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", selectionChange: "selectionChange" }, queries: [{ propertyName: "optionDef", first: true, predicate: KtSelectOptionDef, descendants: true, isSignal: true }, { propertyName: "triggerDef", first: true, predicate: KtSelectTriggerDef, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"fieldErrors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div class=\"kt-select\" [class.kt-select--open]=\"expanded()\">\n <button\n #combobox=\"ngCombobox\"\n #trigger\n ngCombobox\n ktFieldControl\n type=\"button\"\n class=\"kt-field-box kt-select__trigger\"\n [(expanded)]=\"expanded\"\n [disabled]=\"disabled()\"\n [softDisabled]=\"false\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n <span class=\"kt-select__value\">\n @if (triggerDef(); as def) {\n <ng-container [ngTemplateOutlet]=\"def.template\" [ngTemplateOutletContext]=\"{ $implicit: selectedOption() }\" />\n } @else if (selectedOption(); as option) {\n {{ labelOf(option) }}\n } @else {\n <span class=\"kt-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n </span>\n <span class=\"kt-select__arrow\" aria-hidden=\"true\">arrow_drop_down</span>\n </button>\n\n <!-- Un seul Popover (top-layer). CSS @media : dropdown ancr\u00E9 (desktop) \u2194 bottom-sheet (t\u00E9l\u00E9phone).\n Pas de <dialog>.showModal() : ce combobox se ferme au blur, et showModal volerait le focus. -->\n <ng-template ngComboboxPopup [combobox]=\"combobox\" [popupType]=\"filterable() ? 'dialog' : 'listbox'\">\n <div #popup popover=\"manual\" class=\"kt-select__popup\" [class.kt-select__popup--sheet]=\"compact()\">\n @if (compact()) {\n <div class=\"kt-select__sheet-scrim\" aria-hidden=\"true\" (click)=\"expanded.set(false)\"></div>\n }\n <div class=\"kt-select__sheet-card\">\n @if (compact()) {\n <div\n class=\"kt-select__sheet-grab\"\n aria-hidden=\"true\"\n (pointerdown)=\"onDragStart($event)\"\n (mousedown)=\"$event.preventDefault()\"\n ></div>\n <header class=\"kt-select__sheet-header\">\n <span class=\"kt-select__sheet-title\">{{ label() }}</span>\n <button\n type=\"button\"\n class=\"kt-select__sheet-close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"expanded.set(false)\"\n >\n <span class=\"kt-select__sheet-close-icon\" aria-hidden=\"true\">close</span>\n </button>\n </header>\n }\n @if (filterable()) {\n <!-- Mode filtrable : le widget combobox est le panneau ENTIER (champ + annonce + liste).\n Indispensable : un focus pos\u00E9 hors de l'\u00E9l\u00E9ment ngComboboxWidget ferme le popup\n (closePopupOnBlur de @angular/aria) \u2014 le champ doit donc vivre dans ce sous-arbre. -->\n <div\n ngComboboxWidget\n role=\"dialog\"\n class=\"kt-select__panel\"\n [id]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [activeDescendant]=\"lb.activeDescendant()\"\n >\n <div class=\"kt-select__filter\">\n <input\n #filterInput\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n class=\"kt-select__filter-input\"\n autocomplete=\"off\"\n [placeholder]=\"resolvedFilterPlaceholder()\"\n [attr.aria-label]=\"resolvedFilterLabel()\"\n [attr.aria-controls]=\"lb.id()\"\n [attr.aria-activedescendant]=\"lb.activeDescendant()\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeydown($event)\"\n />\n </div>\n <!-- Nombre de r\u00E9sultats annonc\u00E9 (diff\u00E9r\u00E9) aux lecteurs d'\u00E9cran : une liste qui\n r\u00E9tr\u00E9cit en silence est per\u00E7ue comme un bug (tests utilisateurs S. Higley). -->\n <div class=\"kt-select__sr-only\" role=\"status\" aria-live=\"polite\">{{ announcedCount() }}</div>\n <ul\n #lb=\"ngListbox\"\n #listboxEl\n ngListbox\n [focusMode]=\"compact() && !filterable() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n >\n @for (item of displayedOptions(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n @if (filterable() && filteredOptions().length > maxVisibleOptions()) {\n <li class=\"kt-select__truncated-info\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedTruncatedResultsText()(maxVisibleOptions(), filteredOptions().length) }}\n </li>\n }\n </ul>\n </div>\n } @else {\n <ul\n #listbox=\"ngListbox\"\n #listboxEl\n ngComboboxWidget\n ngListbox\n [focusMode]=\"compact() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [activeDescendant]=\"listbox.activeDescendant()\"\n >\n @for (item of options(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, selected: !!opt.selected(), active: opt.active() }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n </ul>\n }\n </div>\n </div>\n </ng-template>\n </div>\n</kt-field>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-select{position:relative}.kt-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--field-control-gap, .5rem);inline-size:100%;cursor:pointer;text-align:start;font:inherit;color:var(--field-color, inherit)}.kt-select__trigger:disabled{cursor:not-allowed;background:var(--field-disabled-bg, #f1f3f4);color:color-mix(in srgb,currentColor 50%,transparent)}.kt-select__value{flex:1;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-select__placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__arrow{flex:none;font-family:Material Symbols Outlined;font-size:1.25em;line-height:1;font-feature-settings:\"liga\";color:var(--field-icon-color, #5f6368);transition:var(--select-arrow-transition, transform .12s ease);-webkit-font-smoothing:antialiased}.kt-select--open .kt-select__arrow{transform:rotate(180deg)}.kt-select__popup{box-sizing:border-box;display:flex;flex-direction:column;margin:0;padding:0;border-width:var(--select-popup-border-width, var(--field-border-width, 1px));border-style:var(--field-border-style, solid);border-color:var(--field-border-color, #c4c7c5);border-radius:var(--field-radius, 8px);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);box-shadow:var(--select-popup-shadow, 0 4px 12px rgb(0 0 0 / 12%));-webkit-backdrop-filter:var(--select-popup-backdrop-filter, none);backdrop-filter:var(--select-popup-backdrop-filter, none);max-block-size:var(--select-popup-max-height, 16rem);overflow:hidden}.kt-select__sheet-card{display:contents}.kt-select__popup:not(:popover-open){display:none!important}.kt-select__popup:popover-open{animation:var(--select-popup-enter-animation, none)}@media(prefers-reduced-motion:reduce){.kt-select__popup:popover-open{animation:none}}.kt-select__listbox{flex:1 1 auto;min-block-size:0;margin:0;padding:.25rem;list-style:none;overflow-y:auto}.kt-select__panel{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0}.kt-select__filter{flex:none;padding:.5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__filter-input{box-sizing:border-box;inline-size:100%;min-block-size:var(--field-min-height, 44px);padding:.375rem .625rem;border:var(--field-border-width, 1px) var(--field-border-style, solid) var(--field-border-color, #c4c7c5);border-radius:calc(var(--field-radius, 8px) * .75);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);font:inherit;appearance:none}.kt-select__filter-input::placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__filter-input:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-1px}.kt-select__filter-input::-webkit-search-cancel-button{appearance:none;inline-size:1rem;block-size:1rem;margin-inline-start:.375rem;cursor:pointer;background-color:var(--field-icon-color, #5f6368);-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat;mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat}.kt-select__sr-only{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@supports (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto;top:anchor(bottom);left:anchor(left);margin-block-start:.25rem;min-inline-size:anchor-size(width);width:max-content;max-inline-size:min(90vw,28rem);position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline}}@supports not (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto 0 0;inline-size:100%;max-inline-size:100%;border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0}}.kt-select__popup--sheet{position:fixed!important;inset:0!important;inline-size:100%!important;max-inline-size:100%!important;min-inline-size:0!important;width:100%!important;height:100%!important;max-block-size:none!important;margin:0!important;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important;border-radius:0!important;overflow:visible!important;display:flex;flex-direction:column!important;justify-content:flex-end!important}.kt-select__popup--sheet .kt-select__sheet-card{position:relative;z-index:2;display:flex;flex-direction:column;background:var(--select-popup-bg, var(--kt-surface, #fff));border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0;box-shadow:var(--kt-sheet-shadow, 0 -4px 16px rgb(0 0 0 / 12%));max-block-size:var(--kt-sheet-max-block-size, 85svh);width:100%;overflow:hidden;translate:0 0;transition:translate var(--kt-sheet-anim-duration, .12s) ease}.kt-select__popup--sheet .kt-select__sheet-card:has(.kt-select__filter){block-size:var(--kt-sheet-max-block-size, 85svh);max-block-size:var(--kt-sheet-max-block-size, 85svh)}.kt-select__popup--sheet:popover-open .kt-select__sheet-card{translate:0 0;animation:var(--select-sheet-enter-animation, kt-sheet-in var(--kt-sheet-anim-duration, .12s) ease)}.kt-select__popup--sheet .kt-select__sheet-card.kt-select__popup--dragging{transition:none}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-card,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-card{translate:0 100%;transition:translate var(--kt-sheet-exit-duration, 90ms) cubic-bezier(.4,0,.2,1)}::ng-deep .kt-select__popup--sheet::backdrop{display:none!important}.kt-select__sheet-scrim{display:none}.kt-select__popup--sheet .kt-select__sheet-scrim{display:block;position:fixed;inset:0;z-index:1;background:var(--kt-sheet-scrim, rgb(0 0 0 / 40%));opacity:0;pointer-events:auto;transition:opacity var(--kt-sheet-anim-duration, .12s) ease,overlay var(--kt-sheet-anim-duration, .12s) allow-discrete,display var(--kt-sheet-anim-duration, .12s) allow-discrete}.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:1}@starting-style{.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:0}}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-scrim,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-scrim{opacity:0;transition:opacity var(--kt-sheet-exit-duration, 90ms) ease}.kt-select__popup--sheet .kt-select__option{--select-option-min-height: 44px;padding-block:.625rem}.kt-select__popup--sheet .kt-select__filter-input{font-size:1rem;min-block-size:44px}@media(prefers-reduced-motion:reduce){.kt-select__popup--sheet .kt-select__sheet-card,.kt-select__popup--sheet .kt-select__sheet-scrim{transition:none;translate:0 0}}.kt-select__sheet-grab{display:flex;align-items:center;justify-content:center;flex:none;block-size:44px;cursor:grab;touch-action:none}.kt-select__sheet-grab:active{cursor:grabbing}.kt-select__sheet-grab:before{content:\"\";inline-size:2.25rem;block-size:.25rem;border-radius:999px;background:var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5))}.kt-select__sheet-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex:none;padding-block:.5rem;padding-inline:1rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__sheet-title{font-weight:600}.kt-select__sheet-close{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:44px;block-size:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__sheet-close:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:2px}.kt-select__sheet-close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.5rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__option{display:flex;align-items:center;gap:.5rem;box-sizing:border-box;min-block-size:var( --select-option-min-height, 44px );padding:.5rem .625rem;border-radius:6px;cursor:pointer}.kt-select__option[aria-selected=true]{background:var(--select-option-selected-bg, color-mix(in srgb, var(--kt-primary, #0b57d0) 14%, transparent));color:var(--select-option-selected-color, inherit);font-weight:var(--select-option-selected-weight, 600)}.kt-select__option--active:not([aria-disabled=true]),.kt-select__option:hover:not([aria-disabled=true]){background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__option[aria-disabled=true]{opacity:.5;cursor:not-allowed}.kt-select__empty{padding:.5rem .625rem;color:var(--field-hint-color, #5f6368)}.kt-select__truncated-info{box-sizing:border-box;padding:.5rem .625rem;font-size:.875rem;font-style:italic;color:var(--field-hint-color, #5f6368);border-block-start:1px solid var(--field-border-color, #c4c7c5);pointer-events:none}}\n"], dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: Combobox, selector: "[ngCombobox]", inputs: ["disabled", "softDisabled", "alwaysExpanded", "tabindex", "expanded", "value", "inlineSuggestion"], outputs: ["expandedChange", "valueChange"], exportAs: ["ngCombobox"] }, { kind: "directive", type: ComboboxPopup, selector: "ng-template[ngComboboxPopup]", inputs: ["combobox", "popupType"], exportAs: ["ngComboboxPopup"] }, { kind: "directive", type: ComboboxWidget, selector: "[ngComboboxWidget]", inputs: ["activeDescendant"], exportAs: ["ngComboboxWidget"] }, { kind: "directive", type: Listbox, selector: "[ngListbox]", inputs: ["id", "orientation", "multi", "wrap", "softDisabled", "focusMode", "selectionMode", "typeaheadDelay", "disabled", "readonly", "tabindex", "value"], outputs: ["valueChange"], exportAs: ["ngListbox"] }, { kind: "directive", type: Option, selector: "[ngOption]", inputs: ["id", "value", "disabled", "label"], exportAs: ["ngOption"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2060
|
+
}
|
|
2061
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelect, decorators: [{
|
|
2062
|
+
type: Component,
|
|
2063
|
+
args: [{ selector: 'kt-select', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KtField, KtFieldControl, NgTemplateOutlet, Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option], template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"fieldErrors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div class=\"kt-select\" [class.kt-select--open]=\"expanded()\">\n <button\n #combobox=\"ngCombobox\"\n #trigger\n ngCombobox\n ktFieldControl\n type=\"button\"\n class=\"kt-field-box kt-select__trigger\"\n [(expanded)]=\"expanded\"\n [disabled]=\"disabled()\"\n [softDisabled]=\"false\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n <span class=\"kt-select__value\">\n @if (triggerDef(); as def) {\n <ng-container [ngTemplateOutlet]=\"def.template\" [ngTemplateOutletContext]=\"{ $implicit: selectedOption() }\" />\n } @else if (selectedOption(); as option) {\n {{ labelOf(option) }}\n } @else {\n <span class=\"kt-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n </span>\n <span class=\"kt-select__arrow\" aria-hidden=\"true\">arrow_drop_down</span>\n </button>\n\n <!-- Un seul Popover (top-layer). CSS @media : dropdown ancr\u00E9 (desktop) \u2194 bottom-sheet (t\u00E9l\u00E9phone).\n Pas de <dialog>.showModal() : ce combobox se ferme au blur, et showModal volerait le focus. -->\n <ng-template ngComboboxPopup [combobox]=\"combobox\" [popupType]=\"filterable() ? 'dialog' : 'listbox'\">\n <div #popup popover=\"manual\" class=\"kt-select__popup\" [class.kt-select__popup--sheet]=\"compact()\">\n @if (compact()) {\n <div class=\"kt-select__sheet-scrim\" aria-hidden=\"true\" (click)=\"expanded.set(false)\"></div>\n }\n <div class=\"kt-select__sheet-card\">\n @if (compact()) {\n <div\n class=\"kt-select__sheet-grab\"\n aria-hidden=\"true\"\n (pointerdown)=\"onDragStart($event)\"\n (mousedown)=\"$event.preventDefault()\"\n ></div>\n <header class=\"kt-select__sheet-header\">\n <span class=\"kt-select__sheet-title\">{{ label() }}</span>\n <button\n type=\"button\"\n class=\"kt-select__sheet-close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"expanded.set(false)\"\n >\n <span class=\"kt-select__sheet-close-icon\" aria-hidden=\"true\">close</span>\n </button>\n </header>\n }\n @if (filterable()) {\n <!-- Mode filtrable : le widget combobox est le panneau ENTIER (champ + annonce + liste).\n Indispensable : un focus pos\u00E9 hors de l'\u00E9l\u00E9ment ngComboboxWidget ferme le popup\n (closePopupOnBlur de @angular/aria) \u2014 le champ doit donc vivre dans ce sous-arbre. -->\n <div\n ngComboboxWidget\n role=\"dialog\"\n class=\"kt-select__panel\"\n [id]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [activeDescendant]=\"lb.activeDescendant()\"\n >\n <div class=\"kt-select__filter\">\n <input\n #filterInput\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n class=\"kt-select__filter-input\"\n autocomplete=\"off\"\n [placeholder]=\"resolvedFilterPlaceholder()\"\n [attr.aria-label]=\"resolvedFilterLabel()\"\n [attr.aria-controls]=\"lb.id()\"\n [attr.aria-activedescendant]=\"lb.activeDescendant()\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeydown($event)\"\n />\n </div>\n <!-- Nombre de r\u00E9sultats annonc\u00E9 (diff\u00E9r\u00E9) aux lecteurs d'\u00E9cran : une liste qui\n r\u00E9tr\u00E9cit en silence est per\u00E7ue comme un bug (tests utilisateurs S. Higley). -->\n <div class=\"kt-select__sr-only\" role=\"status\" aria-live=\"polite\">{{ announcedCount() }}</div>\n <ul\n #lb=\"ngListbox\"\n #listboxEl\n ngListbox\n [focusMode]=\"compact() && !filterable() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n >\n @for (item of displayedOptions(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n @if (filterable() && filteredOptions().length > maxVisibleOptions()) {\n <li class=\"kt-select__truncated-info\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedTruncatedResultsText()(maxVisibleOptions(), filteredOptions().length) }}\n </li>\n }\n </ul>\n </div>\n } @else {\n <ul\n #listbox=\"ngListbox\"\n #listboxEl\n ngComboboxWidget\n ngListbox\n [focusMode]=\"compact() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [activeDescendant]=\"listbox.activeDescendant()\"\n >\n @for (item of options(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, selected: !!opt.selected(), active: opt.active() }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n </ul>\n }\n </div>\n </div>\n </ng-template>\n </div>\n</kt-field>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-select{position:relative}.kt-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--field-control-gap, .5rem);inline-size:100%;cursor:pointer;text-align:start;font:inherit;color:var(--field-color, inherit)}.kt-select__trigger:disabled{cursor:not-allowed;background:var(--field-disabled-bg, #f1f3f4);color:color-mix(in srgb,currentColor 50%,transparent)}.kt-select__value{flex:1;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-select__placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__arrow{flex:none;font-family:Material Symbols Outlined;font-size:1.25em;line-height:1;font-feature-settings:\"liga\";color:var(--field-icon-color, #5f6368);transition:var(--select-arrow-transition, transform .12s ease);-webkit-font-smoothing:antialiased}.kt-select--open .kt-select__arrow{transform:rotate(180deg)}.kt-select__popup{box-sizing:border-box;display:flex;flex-direction:column;margin:0;padding:0;border-width:var(--select-popup-border-width, var(--field-border-width, 1px));border-style:var(--field-border-style, solid);border-color:var(--field-border-color, #c4c7c5);border-radius:var(--field-radius, 8px);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);box-shadow:var(--select-popup-shadow, 0 4px 12px rgb(0 0 0 / 12%));-webkit-backdrop-filter:var(--select-popup-backdrop-filter, none);backdrop-filter:var(--select-popup-backdrop-filter, none);max-block-size:var(--select-popup-max-height, 16rem);overflow:hidden}.kt-select__sheet-card{display:contents}.kt-select__popup:not(:popover-open){display:none!important}.kt-select__popup:popover-open{animation:var(--select-popup-enter-animation, none)}@media(prefers-reduced-motion:reduce){.kt-select__popup:popover-open{animation:none}}.kt-select__listbox{flex:1 1 auto;min-block-size:0;margin:0;padding:.25rem;list-style:none;overflow-y:auto}.kt-select__panel{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0}.kt-select__filter{flex:none;padding:.5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__filter-input{box-sizing:border-box;inline-size:100%;min-block-size:var(--field-min-height, 44px);padding:.375rem .625rem;border:var(--field-border-width, 1px) var(--field-border-style, solid) var(--field-border-color, #c4c7c5);border-radius:calc(var(--field-radius, 8px) * .75);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);font:inherit;appearance:none}.kt-select__filter-input::placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__filter-input:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-1px}.kt-select__filter-input::-webkit-search-cancel-button{appearance:none;inline-size:1rem;block-size:1rem;margin-inline-start:.375rem;cursor:pointer;background-color:var(--field-icon-color, #5f6368);-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat;mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat}.kt-select__sr-only{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@supports (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto;top:anchor(bottom);left:anchor(left);margin-block-start:.25rem;min-inline-size:anchor-size(width);width:max-content;max-inline-size:min(90vw,28rem);position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline}}@supports not (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto 0 0;inline-size:100%;max-inline-size:100%;border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0}}.kt-select__popup--sheet{position:fixed!important;inset:0!important;inline-size:100%!important;max-inline-size:100%!important;min-inline-size:0!important;width:100%!important;height:100%!important;max-block-size:none!important;margin:0!important;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important;border-radius:0!important;overflow:visible!important;display:flex;flex-direction:column!important;justify-content:flex-end!important}.kt-select__popup--sheet .kt-select__sheet-card{position:relative;z-index:2;display:flex;flex-direction:column;background:var(--select-popup-bg, var(--kt-surface, #fff));border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0;box-shadow:var(--kt-sheet-shadow, 0 -4px 16px rgb(0 0 0 / 12%));max-block-size:var(--kt-sheet-max-block-size, 85svh);width:100%;overflow:hidden;translate:0 0;transition:translate var(--kt-sheet-anim-duration, .12s) ease}.kt-select__popup--sheet .kt-select__sheet-card:has(.kt-select__filter){block-size:var(--kt-sheet-max-block-size, 85svh);max-block-size:var(--kt-sheet-max-block-size, 85svh)}.kt-select__popup--sheet:popover-open .kt-select__sheet-card{translate:0 0;animation:var(--select-sheet-enter-animation, kt-sheet-in var(--kt-sheet-anim-duration, .12s) ease)}.kt-select__popup--sheet .kt-select__sheet-card.kt-select__popup--dragging{transition:none}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-card,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-card{translate:0 100%;transition:translate var(--kt-sheet-exit-duration, 90ms) cubic-bezier(.4,0,.2,1)}::ng-deep .kt-select__popup--sheet::backdrop{display:none!important}.kt-select__sheet-scrim{display:none}.kt-select__popup--sheet .kt-select__sheet-scrim{display:block;position:fixed;inset:0;z-index:1;background:var(--kt-sheet-scrim, rgb(0 0 0 / 40%));opacity:0;pointer-events:auto;transition:opacity var(--kt-sheet-anim-duration, .12s) ease,overlay var(--kt-sheet-anim-duration, .12s) allow-discrete,display var(--kt-sheet-anim-duration, .12s) allow-discrete}.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:1}@starting-style{.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:0}}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-scrim,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-scrim{opacity:0;transition:opacity var(--kt-sheet-exit-duration, 90ms) ease}.kt-select__popup--sheet .kt-select__option{--select-option-min-height: 44px;padding-block:.625rem}.kt-select__popup--sheet .kt-select__filter-input{font-size:1rem;min-block-size:44px}@media(prefers-reduced-motion:reduce){.kt-select__popup--sheet .kt-select__sheet-card,.kt-select__popup--sheet .kt-select__sheet-scrim{transition:none;translate:0 0}}.kt-select__sheet-grab{display:flex;align-items:center;justify-content:center;flex:none;block-size:44px;cursor:grab;touch-action:none}.kt-select__sheet-grab:active{cursor:grabbing}.kt-select__sheet-grab:before{content:\"\";inline-size:2.25rem;block-size:.25rem;border-radius:999px;background:var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5))}.kt-select__sheet-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex:none;padding-block:.5rem;padding-inline:1rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__sheet-title{font-weight:600}.kt-select__sheet-close{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:44px;block-size:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__sheet-close:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:2px}.kt-select__sheet-close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.5rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__option{display:flex;align-items:center;gap:.5rem;box-sizing:border-box;min-block-size:var( --select-option-min-height, 44px );padding:.5rem .625rem;border-radius:6px;cursor:pointer}.kt-select__option[aria-selected=true]{background:var(--select-option-selected-bg, color-mix(in srgb, var(--kt-primary, #0b57d0) 14%, transparent));color:var(--select-option-selected-color, inherit);font-weight:var(--select-option-selected-weight, 600)}.kt-select__option--active:not([aria-disabled=true]),.kt-select__option:hover:not([aria-disabled=true]){background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__option[aria-disabled=true]{opacity:.5;cursor:not-allowed}.kt-select__empty{padding:.5rem .625rem;color:var(--field-hint-color, #5f6368)}.kt-select__truncated-info{box-sizing:border-box;padding:.5rem .625rem;font-size:.875rem;font-style:italic;color:var(--field-hint-color, #5f6368);border-block-start:1px solid var(--field-border-color, #c4c7c5);pointer-events:none}}\n"] }]
|
|
2064
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtSelectOptionDef), { isSignal: true }] }], triggerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtSelectTriggerDef), { isSignal: true }] }] } });
|
|
2065
|
+
|
|
2066
|
+
/** Configure les `kt-select` d'un sous-arbre via DI (équivalent du Context provider de react-select).
|
|
2067
|
+
Alternative à un provider global de `KT_SELECT_CONFIG` : on pose la directive sur un conteneur et
|
|
2068
|
+
tous les `kt-select` descendants en héritent.
|
|
2069
|
+
|
|
2070
|
+
```html
|
|
2071
|
+
<div [ktSelectConfig]="{ placeholder: 'Choisir…', emptyText: 'Aucun résultat' }">
|
|
2072
|
+
<kt-select … />
|
|
2073
|
+
</div>
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
La directive EST fournie comme `KT_SELECT_CONFIG` (`useExisting`) : les getters exposent l'objet
|
|
2077
|
+
bindé sous la forme `Partial<KtSelectConfigOptions>` attendue par le composant. */
|
|
2078
|
+
class KtSelectConfig {
|
|
2079
|
+
ktSelectConfig = input({}, /* @ts-ignore */
|
|
2080
|
+
...(ngDevMode ? [{ debugName: "ktSelectConfig" }] : /* istanbul ignore next */ []));
|
|
2081
|
+
/** Config héritée du contexte parent (provider global ou directive englobante) : la
|
|
2082
|
+
directive ne masque que les clés qu'elle définit, le reste continue d'en hériter. */
|
|
2083
|
+
parent = inject(KT_SELECT_CONFIG, { optional: true, skipSelf: true });
|
|
2084
|
+
resolve(key) {
|
|
2085
|
+
return this.ktSelectConfig()[key] ?? this.parent?.[key];
|
|
2086
|
+
}
|
|
2087
|
+
get placeholder() {
|
|
2088
|
+
return this.resolve('placeholder');
|
|
2089
|
+
}
|
|
2090
|
+
get emptyText() {
|
|
2091
|
+
return this.resolve('emptyText');
|
|
2092
|
+
}
|
|
2093
|
+
get closeLabel() {
|
|
2094
|
+
return this.resolve('closeLabel');
|
|
2095
|
+
}
|
|
2096
|
+
get closeOnSelect() {
|
|
2097
|
+
return this.resolve('closeOnSelect');
|
|
2098
|
+
}
|
|
2099
|
+
get filterPlaceholder() {
|
|
2100
|
+
return this.resolve('filterPlaceholder');
|
|
2101
|
+
}
|
|
2102
|
+
get filterLabel() {
|
|
2103
|
+
return this.resolve('filterLabel');
|
|
2104
|
+
}
|
|
2105
|
+
get filterResultsText() {
|
|
2106
|
+
return this.resolve('filterResultsText');
|
|
2107
|
+
}
|
|
2108
|
+
get removeItemLabel() {
|
|
2109
|
+
return this.resolve('removeItemLabel');
|
|
2110
|
+
}
|
|
2111
|
+
get selectedItemsLabel() {
|
|
2112
|
+
return this.resolve('selectedItemsLabel');
|
|
2113
|
+
}
|
|
2114
|
+
get selectionSummaryText() {
|
|
2115
|
+
return this.resolve('selectionSummaryText');
|
|
2116
|
+
}
|
|
2117
|
+
get itemRemovedText() {
|
|
2118
|
+
return this.resolve('itemRemovedText');
|
|
2119
|
+
}
|
|
2120
|
+
get selectionCountText() {
|
|
2121
|
+
return this.resolve('selectionCountText');
|
|
2122
|
+
}
|
|
2123
|
+
get selectAllLabel() {
|
|
2124
|
+
return this.resolve('selectAllLabel');
|
|
2125
|
+
}
|
|
2126
|
+
get clearAllLabel() {
|
|
2127
|
+
return this.resolve('clearAllLabel');
|
|
2128
|
+
}
|
|
2129
|
+
get moreChipsLabel() {
|
|
2130
|
+
return this.resolve('moreChipsLabel');
|
|
2131
|
+
}
|
|
2132
|
+
get lessChipsLabel() {
|
|
2133
|
+
return this.resolve('lessChipsLabel');
|
|
2134
|
+
}
|
|
2135
|
+
get truncatedResultsText() {
|
|
2136
|
+
return this.resolve('truncatedResultsText');
|
|
2137
|
+
}
|
|
2138
|
+
get truncatedResultsAnnouncement() {
|
|
2139
|
+
return this.resolve('truncatedResultsAnnouncement');
|
|
2140
|
+
}
|
|
2141
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectConfig, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2142
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtSelectConfig, isStandalone: true, selector: "[ktSelectConfig]", inputs: { ktSelectConfig: { classPropertyName: "ktSelectConfig", publicName: "ktSelectConfig", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: KT_SELECT_CONFIG, useExisting: KtSelectConfig }], ngImport: i0 });
|
|
2143
|
+
}
|
|
2144
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtSelectConfig, decorators: [{
|
|
2145
|
+
type: Directive,
|
|
2146
|
+
args: [{
|
|
2147
|
+
selector: '[ktSelectConfig]',
|
|
2148
|
+
providers: [{ provide: KT_SELECT_CONFIG, useExisting: KtSelectConfig }],
|
|
2149
|
+
}]
|
|
2150
|
+
}], propDecorators: { ktSelectConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktSelectConfig", required: false }] }] } });
|
|
2151
|
+
|
|
2152
|
+
/** Portée de transition des chips. Fourni par `ChipList` (via `useExisting`) ; injecté en
|
|
2153
|
+
optionnel par `Chip` pour ne porter un `view-transition-name` QUE pendant la transition
|
|
2154
|
+
de SA liste — sinon tous les chips de la page deviendraient des groupes indépendants et
|
|
2155
|
+
« glisseraient » individuellement à chaque reflow déclenché par une autre liste.
|
|
2156
|
+
Fichier séparé : évite l'import circulaire Chip ↔ ChipList. */
|
|
2157
|
+
class ChipTransitionScope {
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* Pilule individuelle (tag). Utilisable seule (tag statique) ou dans `kt-chip-list`.
|
|
2162
|
+
* Le label vient de la projection de contenu ; `removable` ajoute un bouton « retirer »
|
|
2163
|
+
* (24px visibles, cible 44px via ::after — technique des boutons icon-only).
|
|
2164
|
+
*
|
|
2165
|
+
* @example
|
|
2166
|
+
* ```html
|
|
2167
|
+
* <kt-chip>Angular</kt-chip>
|
|
2168
|
+
* <kt-chip removable removeLabel="Retirer Angular" (remove)="onRemove()">Angular</kt-chip>
|
|
2169
|
+
* ```
|
|
2170
|
+
*/
|
|
2171
|
+
class KtChip {
|
|
2172
|
+
scope = inject(ChipTransitionScope, { optional: true });
|
|
2173
|
+
idGen = inject(KtIdGenerator);
|
|
2174
|
+
/** Nom de View Transition propre au chip (détail interne, posé en host binding). */
|
|
2175
|
+
viewTransitionName = `chip-${this.idGen.generateId('chip')}`;
|
|
2176
|
+
/** Affiche le bouton « retirer » (sinon tag statique sans bouton). @default false */
|
|
2177
|
+
removable = input(false, { ...(ngDevMode ? { debugName: "removable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2178
|
+
/** Chip désactivé (bouton « retirer » inactif). @default false */
|
|
2179
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2180
|
+
/** Libellé accessible du bouton « retirer » (inclure le nom de l'item : « Remove X »). @default 'Remove' */
|
|
2181
|
+
removeLabel = input('Remove', /* @ts-ignore */
|
|
2182
|
+
...(ngDevMode ? [{ debugName: "removeLabel" }] : /* istanbul ignore next */ []));
|
|
2183
|
+
/** Émis au clic sur le bouton « retirer » (le parent décide de retirer le chip). */
|
|
2184
|
+
remove = output();
|
|
2185
|
+
removeBtn = viewChild('removeBtn', /* @ts-ignore */
|
|
2186
|
+
...(ngDevMode ? [{ debugName: "removeBtn" }] : /* istanbul ignore next */ []));
|
|
2187
|
+
/** Focus programmatique du bouton « retirer » (focus management d'`kt-chip-list`). */
|
|
2188
|
+
focusRemove() {
|
|
2189
|
+
this.removeBtn()?.nativeElement.focus();
|
|
2190
|
+
}
|
|
2191
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChip, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2192
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtChip, isStandalone: true, selector: "kt-chip", inputs: { removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove" }, host: { properties: { "style.view-transition-name": "scope?.transitioning() ? viewTransitionName : null", "style.view-transition-class": "\"chip-transition\"" }, classAttribute: "kt-chip" }, viewQueries: [{ propertyName: "removeBtn", first: true, predicate: ["removeBtn"], descendants: true, isSignal: true }], ngImport: i0, template: "<span class=\"kt-chip__label\"><ng-content /></span>\n@if (removable()) {\n <button\n #removeBtn\n type=\"button\"\n class=\"kt-chip__remove\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"removeLabel()\"\n (click)=\"remove.emit()\"\n >\n <span class=\"kt-chip__close-icon\" aria-hidden=\"true\">close</span>\n </button>\n}\n", styles: ["@layer kt-aaa.components{:host{display:inline-flex;align-items:center;gap:var(--chip-gap);padding:var(--chip-padding-y) var(--chip-padding-x);border-radius:var(--chip-radius);background:var(--chip-bg);border:1px solid var(--chip-border);box-shadow:var(--chip-shadow, none);font-size:var(--chip-font-size);color:var(--chip-color)}.kt-chip__label{max-inline-size:12rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-chip__remove{position:relative;display:inline-flex;align-items:center;justify-content:center;inline-size:24px;block-size:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--chip-remove-color);cursor:pointer;font-size:.75rem;transition:var(--chip-remove-transition, background-color .15s ease)}.kt-chip__remove:after{content:\"\";position:absolute;inset:-10px}.kt-chip__remove:hover{background-color:var(--chip-remove-bg-hover);color:var(--chip-remove-color-hover)}:host(:hover){box-shadow:var(--chip-shadow-hover, var(--chip-shadow, none))}.kt-chip__remove:focus-visible{outline:2px solid var(--chip-focus-ring);outline-offset:1px}.kt-chip__remove:disabled{cursor:not-allowed;opacity:.5}.kt-chip__close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1rem;line-height:1;-webkit-font-smoothing:antialiased}@media(pointer:coarse){:host{min-block-size:32px}.kt-chip__remove{inline-size:28px;block-size:28px;font-size:.875rem}.kt-chip__remove:after{inset:-8px}}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2193
|
+
}
|
|
2194
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChip, decorators: [{
|
|
2195
|
+
type: Component,
|
|
2196
|
+
args: [{ selector: 'kt-chip', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2197
|
+
class: 'kt-chip',
|
|
2198
|
+
// Nommé UNIQUEMENT pendant la transition de SA liste (cf. ChipTransitionScope) : un nom
|
|
2199
|
+
// permanent ferait participer tous les chips de la page à chaque View Transition (glissement
|
|
2200
|
+
// individuel au moindre reflow). Chip seul (hors liste) : jamais nommé.
|
|
2201
|
+
'[style.view-transition-name]': 'scope?.transitioning() ? viewTransitionName : null',
|
|
2202
|
+
'[style.view-transition-class]': '"chip-transition"',
|
|
2203
|
+
}, template: "<span class=\"kt-chip__label\"><ng-content /></span>\n@if (removable()) {\n <button\n #removeBtn\n type=\"button\"\n class=\"kt-chip__remove\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"removeLabel()\"\n (click)=\"remove.emit()\"\n >\n <span class=\"kt-chip__close-icon\" aria-hidden=\"true\">close</span>\n </button>\n}\n", styles: ["@layer kt-aaa.components{:host{display:inline-flex;align-items:center;gap:var(--chip-gap);padding:var(--chip-padding-y) var(--chip-padding-x);border-radius:var(--chip-radius);background:var(--chip-bg);border:1px solid var(--chip-border);box-shadow:var(--chip-shadow, none);font-size:var(--chip-font-size);color:var(--chip-color)}.kt-chip__label{max-inline-size:12rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-chip__remove{position:relative;display:inline-flex;align-items:center;justify-content:center;inline-size:24px;block-size:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--chip-remove-color);cursor:pointer;font-size:.75rem;transition:var(--chip-remove-transition, background-color .15s ease)}.kt-chip__remove:after{content:\"\";position:absolute;inset:-10px}.kt-chip__remove:hover{background-color:var(--chip-remove-bg-hover);color:var(--chip-remove-color-hover)}:host(:hover){box-shadow:var(--chip-shadow-hover, var(--chip-shadow, none))}.kt-chip__remove:focus-visible{outline:2px solid var(--chip-focus-ring);outline-offset:1px}.kt-chip__remove:disabled{cursor:not-allowed;opacity:.5}.kt-chip__close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1rem;line-height:1;-webkit-font-smoothing:antialiased}@media(pointer:coarse){:host{min-block-size:32px}.kt-chip__remove{inline-size:28px;block-size:28px;font-size:.875rem}.kt-chip__remove:after{inset:-8px}}}\n"] }]
|
|
2204
|
+
}], propDecorators: { removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], removeBtn: [{ type: i0.ViewChild, args: ['removeBtn', { isSignal: true }] }] } });
|
|
2205
|
+
|
|
2206
|
+
/** Template de rendu custom d'un chip dans `kt-chip-list`.
|
|
2207
|
+
Structurellement compatible avec `MultiSelectChipContext` (forwardable).
|
|
2208
|
+
Contexte : `let-item` = l'item rendu (`T`), `let-remove` = fonction qui le retire.
|
|
2209
|
+
|
|
2210
|
+
```html
|
|
2211
|
+
<kt-chip-list [items]="tags">
|
|
2212
|
+
<ng-template [ktChipItem]="tags" let-tag let-remove="remove">
|
|
2213
|
+
<kt-chip [removable]="true" (remove)="remove()">{{ tag.name }}</kt-chip>
|
|
2214
|
+
</ng-template>
|
|
2215
|
+
</kt-chip-list>
|
|
2216
|
+
``` */
|
|
2217
|
+
class KtChipItemDef {
|
|
2218
|
+
ktChipItem = input.required(/* @ts-ignore */
|
|
2219
|
+
...(ngDevMode ? [{ debugName: "ktChipItem" }] : /* istanbul ignore next */ []));
|
|
2220
|
+
template = inject(TemplateRef);
|
|
2221
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
2222
|
+
return typeof ctx === 'object';
|
|
2223
|
+
}
|
|
2224
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChipItemDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2225
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtChipItemDef, isStandalone: true, selector: "ng-template[ktChipItem]", inputs: { ktChipItem: { classPropertyName: "ktChipItem", publicName: "ktChipItem", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
2226
|
+
}
|
|
2227
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChipItemDef, decorators: [{
|
|
2228
|
+
type: Directive,
|
|
2229
|
+
args: [{ selector: 'ng-template[ktChipItem]' }]
|
|
2230
|
+
}], propDecorators: { ktChipItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktChipItem", required: true }] }] } });
|
|
2231
|
+
|
|
2232
|
+
const KT_CHIPS_CONFIG = new InjectionToken('KT_CHIPS_CONFIG');
|
|
2233
|
+
|
|
2234
|
+
/** Délai avant effacement de l'annonce de retrait (live region). */
|
|
2235
|
+
const STATUS_CLEAR_MS = 2000;
|
|
2236
|
+
/**
|
|
2237
|
+
* Liste de chips révocables (`role="list"`) : repli au-delà de `maxVisible` (« +N more »),
|
|
2238
|
+
* annonces lecteur d'écran (live region TOUJOURS rendue, y compris liste vide) et gestion
|
|
2239
|
+
* du focus au retrait (chip suivant, sinon `emptyFocusTarget`).
|
|
2240
|
+
* La liste ne possède PAS la donnée : le retrait émet `removed` et le parent met à jour
|
|
2241
|
+
* `items` (pattern contrôlé). Rendu custom par chip via `ng-template[ktChipItem]` (projeté)
|
|
2242
|
+
* ou `[chipTemplate]` (TemplateRef forwardé, ex. depuis MultiSelect) — le focus management
|
|
2243
|
+
* ne voit pas les boutons des templates custom (view queries non traversantes) : le repli
|
|
2244
|
+
* `emptyFocusTarget` couvre ce cas.
|
|
2245
|
+
*
|
|
2246
|
+
* @example
|
|
2247
|
+
* ```html
|
|
2248
|
+
* <kt-chip-list [items]="tags()" [maxVisible]="3" (removed)="onRemoved($event)" />
|
|
2249
|
+
* ```
|
|
2250
|
+
*/
|
|
2251
|
+
class KtChipList {
|
|
2252
|
+
config = inject(KT_CHIPS_CONFIG, { optional: true });
|
|
2253
|
+
injector = inject(Injector);
|
|
2254
|
+
host = inject(ElementRef);
|
|
2255
|
+
doc = inject(DOCUMENT);
|
|
2256
|
+
platformId = inject(PLATFORM_ID);
|
|
2257
|
+
cdr = inject(ChangeDetectorRef);
|
|
2258
|
+
/** Vrai pendant la View Transition de CETTE liste : les chips (standards via Chip,
|
|
2259
|
+
custom via tokens.css) ne portent leur `view-transition-name` que dans cette fenêtre. */
|
|
2260
|
+
transitioning = signal(false, /* @ts-ignore */
|
|
2261
|
+
...(ngDevMode ? [{ debugName: "transitioning" }] : /* istanbul ignore next */ []));
|
|
2262
|
+
/** Items affichés sous forme de chips (la liste ne possède pas la donnée : pattern contrôlé). */
|
|
2263
|
+
items = input.required(/* @ts-ignore */
|
|
2264
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
2265
|
+
/** Libellé d'un item : clé OU fonction. @default `label`/`name`, sinon `String(item)` */
|
|
2266
|
+
itemLabel = input(/* @ts-ignore */
|
|
2267
|
+
...(ngDevMode ? [undefined, { debugName: "itemLabel" }] : /* istanbul ignore next */ []));
|
|
2268
|
+
/** Identité d'un item (track du @for) : clé OU fonction. @default `id`/`value`, sinon l'item */
|
|
2269
|
+
itemKey = input(/* @ts-ignore */
|
|
2270
|
+
...(ngDevMode ? [undefined, { debugName: "itemKey" }] : /* istanbul ignore next */ []));
|
|
2271
|
+
/** Chips révocables (bouton « retirer » par chip). @default true */
|
|
2272
|
+
removable = input(true, { ...(ngDevMode ? { debugName: "removable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2273
|
+
/** Liste désactivée (boutons « retirer » présents mais inactifs). @default false */
|
|
2274
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2275
|
+
/** Lecture seule : les boutons « retirer » sont absents (≠ disabled : présents mais inactifs). @default false */
|
|
2276
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2277
|
+
/** Nombre maximal de chips affichés avant repli derrière « +N more ». @default undefined (illimité) */
|
|
2278
|
+
maxVisible = input(/* @ts-ignore */
|
|
2279
|
+
...(ngDevMode ? [undefined, { debugName: "maxVisible" }] : /* istanbul ignore next */ []));
|
|
2280
|
+
/** Libellé accessible de la liste (`aria-label`). Défaut : `KT_CHIPS_CONFIG.listLabel` ou « Selected items ». */
|
|
2281
|
+
listLabel = input(/* @ts-ignore */
|
|
2282
|
+
...(ngDevMode ? [undefined, { debugName: "listLabel" }] : /* istanbul ignore next */ []));
|
|
2283
|
+
/** Construit le libellé du bouton « retirer » à partir du libellé de l'item. @default `« Remove {label} »` */
|
|
2284
|
+
removeItemLabel = input(/* @ts-ignore */
|
|
2285
|
+
...(ngDevMode ? [undefined, { debugName: "removeItemLabel" }] : /* istanbul ignore next */ []));
|
|
2286
|
+
/** Construit le message d'annonce après retrait (live region). @default `« {label} removed »` */
|
|
2287
|
+
itemRemovedText = input(/* @ts-ignore */
|
|
2288
|
+
...(ngDevMode ? [undefined, { debugName: "itemRemovedText" }] : /* istanbul ignore next */ []));
|
|
2289
|
+
/** Construit le libellé du bouton de repli à partir du nombre masqué. @default `« +{n} more »` */
|
|
2290
|
+
moreLabel = input(/* @ts-ignore */
|
|
2291
|
+
...(ngDevMode ? [undefined, { debugName: "moreLabel" }] : /* istanbul ignore next */ []));
|
|
2292
|
+
/** Libellé du bouton qui replie la liste dépliée. Défaut : `KT_CHIPS_CONFIG.lessLabel` ou « Show less ». */
|
|
2293
|
+
lessLabel = input(/* @ts-ignore */
|
|
2294
|
+
...(ngDevMode ? [undefined, { debugName: "lessLabel" }] : /* istanbul ignore next */ []));
|
|
2295
|
+
/** Template custom forwardé (alternative au `ng-template[ktChipItem]` projeté). @default null */
|
|
2296
|
+
chipTemplate = input(null, /* @ts-ignore */
|
|
2297
|
+
...(ngDevMode ? [{ debugName: "chipTemplate" }] : /* istanbul ignore next */ []));
|
|
2298
|
+
/** Élément à focuser quand le dernier chip est retiré (ex. le trigger du champ parent). @default undefined */
|
|
2299
|
+
emptyFocusTarget = input(/* @ts-ignore */
|
|
2300
|
+
...(ngDevMode ? [undefined, { debugName: "emptyFocusTarget" }] : /* istanbul ignore next */ []));
|
|
2301
|
+
/** Émis quand un chip est retiré (le parent doit retirer l'item de `items`).
|
|
2302
|
+
Payload : `{ item, index }` — l'item retiré et son index dans `items` au moment du retrait. */
|
|
2303
|
+
removed = output();
|
|
2304
|
+
itemDef = contentChild(KtChipItemDef, /* @ts-ignore */
|
|
2305
|
+
...(ngDevMode ? [{ debugName: "itemDef" }] : /* istanbul ignore next */ []));
|
|
2306
|
+
chips = viewChildren(KtChip, /* @ts-ignore */
|
|
2307
|
+
...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
|
|
2308
|
+
// --- Textes résolus (input > KT_CHIPS_CONFIG > défaut lib neutre EN) ---
|
|
2309
|
+
resolvedListLabel = computed(() => this.listLabel() ?? this.config?.listLabel ?? 'Selected items', /* @ts-ignore */
|
|
2310
|
+
...(ngDevMode ? [{ debugName: "resolvedListLabel" }] : /* istanbul ignore next */ []));
|
|
2311
|
+
resolvedRemoveItemLabel = computed(() => this.removeItemLabel() ?? this.config?.removeItemLabel ?? ((l) => `Remove ${l}`), /* @ts-ignore */
|
|
2312
|
+
...(ngDevMode ? [{ debugName: "resolvedRemoveItemLabel" }] : /* istanbul ignore next */ []));
|
|
2313
|
+
resolvedItemRemovedText = computed(() => this.itemRemovedText() ?? this.config?.itemRemovedText ?? ((l) => `${l} removed`), /* @ts-ignore */
|
|
2314
|
+
...(ngDevMode ? [{ debugName: "resolvedItemRemovedText" }] : /* istanbul ignore next */ []));
|
|
2315
|
+
resolvedMoreLabel = computed(() => this.moreLabel() ?? this.config?.moreLabel ?? ((n) => `+${n} more`), /* @ts-ignore */
|
|
2316
|
+
...(ngDevMode ? [{ debugName: "resolvedMoreLabel" }] : /* istanbul ignore next */ []));
|
|
2317
|
+
resolvedLessLabel = computed(() => this.lessLabel() ?? this.config?.lessLabel ?? 'Show less', /* @ts-ignore */
|
|
2318
|
+
...(ngDevMode ? [{ debugName: "resolvedLessLabel" }] : /* istanbul ignore next */ []));
|
|
2319
|
+
// --- Accès aux items ---
|
|
2320
|
+
labelAccessor = computed(() => accessor(this.itemLabel(), defaultLabel), /* @ts-ignore */
|
|
2321
|
+
...(ngDevMode ? [{ debugName: "labelAccessor" }] : /* istanbul ignore next */ []));
|
|
2322
|
+
keyAccessor = computed(() => accessor(this.itemKey(), defaultIdentity), /* @ts-ignore */
|
|
2323
|
+
...(ngDevMode ? [{ debugName: "keyAccessor" }] : /* istanbul ignore next */ []));
|
|
2324
|
+
labelOf(item) {
|
|
2325
|
+
return this.labelAccessor()(item);
|
|
2326
|
+
}
|
|
2327
|
+
keyOf(item) {
|
|
2328
|
+
return this.keyAccessor()(item);
|
|
2329
|
+
}
|
|
2330
|
+
removeLabelFor(item) {
|
|
2331
|
+
return this.resolvedRemoveItemLabel()(this.labelOf(item));
|
|
2332
|
+
}
|
|
2333
|
+
/** Template effectif : contenu projeté (ktChipItem) prioritaire sur l'input forwardé. */
|
|
2334
|
+
effectiveTemplate = computed(() => this.itemDef()?.template ?? this.chipTemplate(), /* @ts-ignore */
|
|
2335
|
+
...(ngDevMode ? [{ debugName: "effectiveTemplate" }] : /* istanbul ignore next */ []));
|
|
2336
|
+
showRemove = computed(() => this.removable() && !this.readonly(), /* @ts-ignore */
|
|
2337
|
+
...(ngDevMode ? [{ debugName: "showRemove" }] : /* istanbul ignore next */ []));
|
|
2338
|
+
// --- Repli au-delà de maxVisible ---
|
|
2339
|
+
expanded = signal(false, /* @ts-ignore */
|
|
2340
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
2341
|
+
overflow = computed(() => {
|
|
2342
|
+
const max = this.maxVisible();
|
|
2343
|
+
return max !== undefined && this.items().length > max;
|
|
2344
|
+
}, /* @ts-ignore */
|
|
2345
|
+
...(ngDevMode ? [{ debugName: "overflow" }] : /* istanbul ignore next */ []));
|
|
2346
|
+
hiddenCount = computed(() => {
|
|
2347
|
+
const max = this.maxVisible();
|
|
2348
|
+
if (max === undefined || this.expanded())
|
|
2349
|
+
return 0;
|
|
2350
|
+
return Math.max(0, this.items().length - max);
|
|
2351
|
+
}, /* @ts-ignore */
|
|
2352
|
+
...(ngDevMode ? [{ debugName: "hiddenCount" }] : /* istanbul ignore next */ []));
|
|
2353
|
+
visibleItems = computed(() => {
|
|
2354
|
+
const all = this.items();
|
|
2355
|
+
const max = this.maxVisible();
|
|
2356
|
+
if (max === undefined || this.expanded() || all.length <= max)
|
|
2357
|
+
return all;
|
|
2358
|
+
return all.slice(0, max);
|
|
2359
|
+
}, /* @ts-ignore */
|
|
2360
|
+
...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
|
|
2361
|
+
// --- Live region (toujours rendue, y compris liste vide) ---
|
|
2362
|
+
status = signal('', /* @ts-ignore */
|
|
2363
|
+
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
2364
|
+
statusTimer;
|
|
2365
|
+
constructor() {
|
|
2366
|
+
// Repli automatique quand la liste repasse sous le seuil.
|
|
2367
|
+
effect(() => {
|
|
2368
|
+
if (this.overflow())
|
|
2369
|
+
return;
|
|
2370
|
+
untracked(() => {
|
|
2371
|
+
this.expanded.set(false);
|
|
2372
|
+
});
|
|
2373
|
+
}, { allowSignalWrites: true });
|
|
2374
|
+
inject(DestroyRef).onDestroy(() => clearTimeout(this.statusTimer));
|
|
2375
|
+
}
|
|
2376
|
+
idGen = inject(KtIdGenerator);
|
|
2377
|
+
moreBtnTransitionName = `chip-list-more-${this.idGen.generateId('chip-list')}`;
|
|
2378
|
+
/** Annonce un message dans la live region de la liste (effacé après ~2 s).
|
|
2379
|
+
Public : permet au parent d'annoncer des actions liées (ex. « tout effacer »). */
|
|
2380
|
+
announce(text) {
|
|
2381
|
+
clearTimeout(this.statusTimer);
|
|
2382
|
+
this.status.set(text);
|
|
2383
|
+
this.statusTimer = setTimeout(() => this.status.set(''), STATUS_CLEAR_MS);
|
|
2384
|
+
}
|
|
2385
|
+
activeTransition = null;
|
|
2386
|
+
/** Exécute `action` dans une View Transition (morph des chips qui se déplacent, entrée/sortie
|
|
2387
|
+
stylées via `::view-transition-*(.chip-transition)` — cf. tokens.css). Transition SCOPÉE à
|
|
2388
|
+
l'élément quand le navigateur le permet (`element.startViewTransition`, Chrome 147+) ; sinon
|
|
2389
|
+
transition document — dans les deux cas, seuls les chips de CETTE liste sont nommés (cf.
|
|
2390
|
+
`transitioning`). Fallback : action directe si l'API est absente ou si l'utilisateur
|
|
2391
|
+
préfère moins de mouvement. */
|
|
2392
|
+
withViewTransition(action) {
|
|
2393
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
2394
|
+
action();
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
// Garde matchMedia : absent en SSR/jsdom (même convention que le gating mobile du select).
|
|
2398
|
+
const prefersReducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
2399
|
+
// Enhancement progressif : transition scopée au sous-arbre de la liste si disponible
|
|
2400
|
+
// (capture moins chère, le reste de la page n'est pas concerné), sinon document entier.
|
|
2401
|
+
const hostEl = this.host.nativeElement;
|
|
2402
|
+
const startViewTransition = hostEl.startViewTransition?.bind(hostEl) ??
|
|
2403
|
+
this.doc.startViewTransition?.bind(this.doc);
|
|
2404
|
+
if (!startViewTransition || prefersReducedMotion) {
|
|
2405
|
+
action();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
// Interactions rapprochées : Chrome DIFFÈRE le callback d'une transition démarrée pendant
|
|
2409
|
+
// qu'une autre est active (UI périmée pour le clic suivant, update parfois perdue — vérifié).
|
|
2410
|
+
// On n'empile donc jamais : la transition en vol est sautée (son callback en attente est
|
|
2411
|
+
// flushé par skipTransition) et l'action s'applique directement, sans animation.
|
|
2412
|
+
if (this.activeTransition) {
|
|
2413
|
+
this.activeTransition.skipTransition();
|
|
2414
|
+
this.activeTransition = null;
|
|
2415
|
+
action();
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
// Nommer les chips de CETTE liste AVANT la capture de l'état « avant » (rendu synchrone
|
|
2419
|
+
// obligatoire : un nom absent du snapshot « avant » ferait passer chaque chip pour un
|
|
2420
|
+
// entrant). Les chips des autres listes restent anonymes => ils suivent le fondu racine.
|
|
2421
|
+
this.transitioning.set(true);
|
|
2422
|
+
this.cdr.detectChanges();
|
|
2423
|
+
const vt = startViewTransition(() => {
|
|
2424
|
+
action();
|
|
2425
|
+
// Rendu synchrone : la View Transition capture l'état "après" à la fin de ce callback.
|
|
2426
|
+
this.cdr.detectChanges();
|
|
2427
|
+
});
|
|
2428
|
+
this.activeTransition = vt;
|
|
2429
|
+
// `ready` rejette (AbortError) si la transition est sautée : attendu, ne pas laisser
|
|
2430
|
+
// remonter en unhandled rejection.
|
|
2431
|
+
vt.ready.catch(() => undefined);
|
|
2432
|
+
vt.finished.finally(() => {
|
|
2433
|
+
if (this.activeTransition === vt)
|
|
2434
|
+
this.activeTransition = null;
|
|
2435
|
+
this.transitioning.set(false);
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
/** Retrait demandé : annonce, émet vers le parent (qui met à jour `items`), puis focus
|
|
2439
|
+
le chip suivant — sinon `emptyFocusTarget` (le bouton focusé disparaît du DOM). */
|
|
2440
|
+
removeAt(item, index) {
|
|
2441
|
+
if (this.disabled() || this.readonly())
|
|
2442
|
+
return;
|
|
2443
|
+
this.announce(this.resolvedItemRemovedText()(this.labelOf(item)));
|
|
2444
|
+
this.withViewTransition(() => {
|
|
2445
|
+
this.removed.emit({ item, index });
|
|
2446
|
+
afterNextRender({
|
|
2447
|
+
read: () => {
|
|
2448
|
+
const hostEl = this.host.nativeElement;
|
|
2449
|
+
const focusables = Array.from(hostEl.querySelectorAll('.kt-chip__remove, .kt-chip-list__item button, .kt-chip-list__item [tabindex="0"]'));
|
|
2450
|
+
const target = focusables[Math.min(index, focusables.length - 1)];
|
|
2451
|
+
if (target) {
|
|
2452
|
+
target.focus();
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
this.emptyFocusTarget()?.focus();
|
|
2456
|
+
},
|
|
2457
|
+
}, { injector: this.injector });
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
removeCallback(item, index) {
|
|
2461
|
+
return () => this.removeAt(item, index);
|
|
2462
|
+
}
|
|
2463
|
+
/** Déplie/replie ; au dépliage, focus sur le premier chip nouvellement révélé. */
|
|
2464
|
+
toggleExpanded() {
|
|
2465
|
+
this.withViewTransition(() => {
|
|
2466
|
+
const expand = !this.expanded();
|
|
2467
|
+
this.expanded.set(expand);
|
|
2468
|
+
if (!expand)
|
|
2469
|
+
return;
|
|
2470
|
+
const firstRevealed = this.maxVisible() ?? 0;
|
|
2471
|
+
afterNextRender({
|
|
2472
|
+
read: () => {
|
|
2473
|
+
const hostEl = this.host.nativeElement;
|
|
2474
|
+
const focusables = Array.from(hostEl.querySelectorAll('.kt-chip__remove, .kt-chip-list__item button, .kt-chip-list__item [tabindex="0"]'));
|
|
2475
|
+
focusables[firstRevealed]?.focus();
|
|
2476
|
+
},
|
|
2477
|
+
}, { injector: this.injector });
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
onKeydown(event) {
|
|
2481
|
+
const key = event.key;
|
|
2482
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)) {
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const hostEl = this.host.nativeElement;
|
|
2486
|
+
const focusables = Array.from(hostEl.querySelectorAll('.kt-chip__remove, .kt-chip-list__item button, .kt-chip-list__item [tabindex="0"], .kt-chip-list__more'));
|
|
2487
|
+
if (focusables.length === 0)
|
|
2488
|
+
return;
|
|
2489
|
+
const active = this.doc.activeElement;
|
|
2490
|
+
const currentIndex = focusables.indexOf(active);
|
|
2491
|
+
let nextIndex = -1;
|
|
2492
|
+
if (key === 'ArrowRight' || key === 'ArrowDown') {
|
|
2493
|
+
nextIndex = currentIndex + 1;
|
|
2494
|
+
}
|
|
2495
|
+
else if (key === 'ArrowLeft' || key === 'ArrowUp') {
|
|
2496
|
+
nextIndex = currentIndex - 1;
|
|
2497
|
+
}
|
|
2498
|
+
else if (key === 'Home') {
|
|
2499
|
+
nextIndex = 0;
|
|
2500
|
+
}
|
|
2501
|
+
else if (key === 'End') {
|
|
2502
|
+
nextIndex = focusables.length - 1;
|
|
2503
|
+
}
|
|
2504
|
+
if (nextIndex >= 0 && nextIndex < focusables.length) {
|
|
2505
|
+
event.preventDefault();
|
|
2506
|
+
focusables[nextIndex].focus();
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChipList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2510
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtChipList, isStandalone: true, selector: "kt-chip-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, itemLabel: { classPropertyName: "itemLabel", publicName: "itemLabel", isSignal: true, isRequired: false, transformFunction: null }, itemKey: { classPropertyName: "itemKey", publicName: "itemKey", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, maxVisible: { classPropertyName: "maxVisible", publicName: "maxVisible", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, removeItemLabel: { classPropertyName: "removeItemLabel", publicName: "removeItemLabel", isSignal: true, isRequired: false, transformFunction: null }, itemRemovedText: { classPropertyName: "itemRemovedText", publicName: "itemRemovedText", isSignal: true, isRequired: false, transformFunction: null }, moreLabel: { classPropertyName: "moreLabel", publicName: "moreLabel", isSignal: true, isRequired: false, transformFunction: null }, lessLabel: { classPropertyName: "lessLabel", publicName: "lessLabel", isSignal: true, isRequired: false, transformFunction: null }, chipTemplate: { classPropertyName: "chipTemplate", publicName: "chipTemplate", isSignal: true, isRequired: false, transformFunction: null }, emptyFocusTarget: { classPropertyName: "emptyFocusTarget", publicName: "emptyFocusTarget", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { removed: "removed" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.data-empty": "items().length === 0 ? '' : null", "attr.data-vt-active": "transitioning() ? '' : null" } }, providers: [{ provide: ChipTransitionScope, useExisting: KtChipList }], queries: [{ propertyName: "itemDef", first: true, predicate: KtChipItemDef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "chips", predicate: KtChip, descendants: true, isSignal: true }], ngImport: i0, template: "@if (items().length > 0) {\n <div class=\"kt-chip-list\">\n <div class=\"kt-chip-list__items\" role=\"list\" [attr.aria-label]=\"resolvedListLabel()\">\n @for (item of visibleItems(); track keyOf(item); let i = $index) {\n @if (effectiveTemplate(); as tpl) {\n <div role=\"listitem\" class=\"kt-chip-list__item\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: item, remove: removeCallback(item, i) }\"\n />\n </div>\n } @else {\n <kt-chip\n role=\"listitem\"\n [removable]=\"showRemove()\"\n [disabled]=\"disabled()\"\n [removeLabel]=\"removeLabelFor(item)\"\n (remove)=\"removeAt(item, i)\"\n >{{ labelOf(item) }}</kt-chip\n >\n }\n }\n </div>\n <!-- Le bouton de repli/d\u00E9pli n'est PAS un chip : hors du role=\"list\" pour ne pas \u00EAtre\n annonc\u00E9 comme un item de la liste (reste dans la m\u00EAme rang\u00E9e flex). -->\n @if (overflow()) {\n <button\n type=\"button\"\n class=\"kt-chip-list__more\"\n [attr.aria-expanded]=\"expanded()\"\n [style.view-transition-name]=\"transitioning() ? moreBtnTransitionName : null\"\n [style.view-transition-class]=\"'chip-transition'\"\n (click)=\"toggleExpanded()\"\n >\n {{ expanded() ? resolvedLessLabel() : resolvedMoreLabel()(hiddenCount()) }}\n </button>\n }\n </div>\n}\n<!-- Annonce des retraits : TOUJOURS rendue (hors du @if, sinon elle dispara\u00EEt avec le dernier chip). -->\n<div class=\"kt-chip-list__status\" role=\"status\" aria-live=\"polite\">{{ status() }}</div>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-chip-list{display:flex;flex-wrap:wrap;gap:.5rem}.kt-chip-list__items,.kt-chip-list__item{display:contents}.kt-chip-list__more{display:inline-flex;align-items:center;gap:var(--chip-gap);padding:var(--chip-padding-y) var(--chip-padding-x);border-radius:var(--chip-radius);background:var(--chip-bg);border:1px solid var(--chip-border);box-shadow:var(--chip-shadow, none);font:inherit;font-size:var(--chip-font-size);color:var(--chip-color);cursor:pointer;min-block-size:24px}.kt-chip-list__more:hover{background:var(--chip-bg-hover);box-shadow:var(--chip-shadow-hover, var(--chip-shadow, none))}.kt-chip-list__more:focus-visible{outline:2px solid var(--chip-focus-ring);outline-offset:1px}.kt-chip-list__status{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@media(pointer:coarse){.kt-chip-list__more{min-block-size:32px}}}\n"], dependencies: [{ kind: "component", type: KtChip, selector: "kt-chip", inputs: ["removable", "disabled", "removeLabel"], outputs: ["remove"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2511
|
+
}
|
|
2512
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtChipList, decorators: [{
|
|
2513
|
+
type: Component,
|
|
2514
|
+
args: [{ selector: 'kt-chip-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KtChip, NgTemplateOutlet], providers: [{ provide: ChipTransitionScope, useExisting: KtChipList }], host: {
|
|
2515
|
+
'[attr.data-empty]': "items().length === 0 ? '' : null",
|
|
2516
|
+
// Marqueur de portée pendant la View Transition : les règles globales (tokens.css) ne
|
|
2517
|
+
// nomment les chips à template custom QUE sous ce marqueur (cf. ChipTransitionScope).
|
|
2518
|
+
'[attr.data-vt-active]': "transitioning() ? '' : null",
|
|
2519
|
+
'(keydown)': 'onKeydown($event)',
|
|
2520
|
+
}, template: "@if (items().length > 0) {\n <div class=\"kt-chip-list\">\n <div class=\"kt-chip-list__items\" role=\"list\" [attr.aria-label]=\"resolvedListLabel()\">\n @for (item of visibleItems(); track keyOf(item); let i = $index) {\n @if (effectiveTemplate(); as tpl) {\n <div role=\"listitem\" class=\"kt-chip-list__item\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: item, remove: removeCallback(item, i) }\"\n />\n </div>\n } @else {\n <kt-chip\n role=\"listitem\"\n [removable]=\"showRemove()\"\n [disabled]=\"disabled()\"\n [removeLabel]=\"removeLabelFor(item)\"\n (remove)=\"removeAt(item, i)\"\n >{{ labelOf(item) }}</kt-chip\n >\n }\n }\n </div>\n <!-- Le bouton de repli/d\u00E9pli n'est PAS un chip : hors du role=\"list\" pour ne pas \u00EAtre\n annonc\u00E9 comme un item de la liste (reste dans la m\u00EAme rang\u00E9e flex). -->\n @if (overflow()) {\n <button\n type=\"button\"\n class=\"kt-chip-list__more\"\n [attr.aria-expanded]=\"expanded()\"\n [style.view-transition-name]=\"transitioning() ? moreBtnTransitionName : null\"\n [style.view-transition-class]=\"'chip-transition'\"\n (click)=\"toggleExpanded()\"\n >\n {{ expanded() ? resolvedLessLabel() : resolvedMoreLabel()(hiddenCount()) }}\n </button>\n }\n </div>\n}\n<!-- Annonce des retraits : TOUJOURS rendue (hors du @if, sinon elle dispara\u00EEt avec le dernier chip). -->\n<div class=\"kt-chip-list__status\" role=\"status\" aria-live=\"polite\">{{ status() }}</div>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-chip-list{display:flex;flex-wrap:wrap;gap:.5rem}.kt-chip-list__items,.kt-chip-list__item{display:contents}.kt-chip-list__more{display:inline-flex;align-items:center;gap:var(--chip-gap);padding:var(--chip-padding-y) var(--chip-padding-x);border-radius:var(--chip-radius);background:var(--chip-bg);border:1px solid var(--chip-border);box-shadow:var(--chip-shadow, none);font:inherit;font-size:var(--chip-font-size);color:var(--chip-color);cursor:pointer;min-block-size:24px}.kt-chip-list__more:hover{background:var(--chip-bg-hover);box-shadow:var(--chip-shadow-hover, var(--chip-shadow, none))}.kt-chip-list__more:focus-visible{outline:2px solid var(--chip-focus-ring);outline-offset:1px}.kt-chip-list__status{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@media(pointer:coarse){.kt-chip-list__more{min-block-size:32px}}}\n"] }]
|
|
2521
|
+
}], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemLabel", required: false }] }], itemKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemKey", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], maxVisible: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisible", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], removeItemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeItemLabel", required: false }] }], itemRemovedText: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemRemovedText", required: false }] }], moreLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "moreLabel", required: false }] }], lessLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lessLabel", required: false }] }], chipTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipTemplate", required: false }] }], emptyFocusTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyFocusTarget", required: false }] }], removed: [{ type: i0.Output, args: ["removed"] }], itemDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtChipItemDef), { isSignal: true }] }], chips: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => KtChip), { isSignal: true }] }] } });
|
|
2522
|
+
|
|
2523
|
+
/** Template de rendu d'une option dans la liste du multi-select. L'input sert à inférer `T` ;
|
|
2524
|
+
`ngTemplateContextGuard` type les variables `let-`.
|
|
2525
|
+
Contexte : `let-option` = l'option (`T`), `let-selected` / `let-active` = booléens.
|
|
2526
|
+
|
|
2527
|
+
```html
|
|
2528
|
+
<kt-multi-select [options]="tags">
|
|
2529
|
+
<ng-template [ktMultiSelectOption]="tags" let-tag let-selected="selected">
|
|
2530
|
+
<tag-badge [tag]="tag" /> @if (selected) { ✓ }
|
|
2531
|
+
</ng-template>
|
|
2532
|
+
</kt-multi-select>
|
|
2533
|
+
``` */
|
|
2534
|
+
class KtMultiSelectOptionDef {
|
|
2535
|
+
ktMultiSelectOption = input.required(/* @ts-ignore */
|
|
2536
|
+
...(ngDevMode ? [{ debugName: "ktMultiSelectOption" }] : /* istanbul ignore next */ []));
|
|
2537
|
+
template = inject(TemplateRef);
|
|
2538
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
2539
|
+
return typeof ctx === 'object';
|
|
2540
|
+
}
|
|
2541
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectOptionDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2542
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMultiSelectOptionDef, isStandalone: true, selector: "ng-template[ktMultiSelectOption]", inputs: { ktMultiSelectOption: { classPropertyName: "ktMultiSelectOption", publicName: "ktMultiSelectOption", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
2543
|
+
}
|
|
2544
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectOptionDef, decorators: [{
|
|
2545
|
+
type: Directive,
|
|
2546
|
+
args: [{ selector: 'ng-template[ktMultiSelectOption]' }]
|
|
2547
|
+
}], propDecorators: { ktMultiSelectOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktMultiSelectOption", required: true }] }] } });
|
|
2548
|
+
/** Template de rendu du trigger (contenu du bouton déclencheur) pour le multi-select.
|
|
2549
|
+
Contexte : `let-options` = le tableau des options sélectionnées (`readonly T[]`).
|
|
2550
|
+
|
|
2551
|
+
```html
|
|
2552
|
+
<kt-multi-select [options]="tags">
|
|
2553
|
+
<ng-template [ktMultiSelectTrigger]="tags" let-options>
|
|
2554
|
+
@if (options.length) { {{ options.length }} sélectionné(s) } @else { Choisir… }
|
|
2555
|
+
</ng-template>
|
|
2556
|
+
</kt-multi-select>
|
|
2557
|
+
``` */
|
|
2558
|
+
class KtMultiSelectTriggerDef {
|
|
2559
|
+
ktMultiSelectTrigger = input.required(/* @ts-ignore */
|
|
2560
|
+
...(ngDevMode ? [{ debugName: "ktMultiSelectTrigger" }] : /* istanbul ignore next */ []));
|
|
2561
|
+
template = inject(TemplateRef);
|
|
2562
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
2563
|
+
return typeof ctx === 'object';
|
|
2564
|
+
}
|
|
2565
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectTriggerDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2566
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMultiSelectTriggerDef, isStandalone: true, selector: "ng-template[ktMultiSelectTrigger]", inputs: { ktMultiSelectTrigger: { classPropertyName: "ktMultiSelectTrigger", publicName: "ktMultiSelectTrigger", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
2567
|
+
}
|
|
2568
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectTriggerDef, decorators: [{
|
|
2569
|
+
type: Directive,
|
|
2570
|
+
args: [{ selector: 'ng-template[ktMultiSelectTrigger]' }]
|
|
2571
|
+
}], propDecorators: { ktMultiSelectTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktMultiSelectTrigger", required: true }] }] } });
|
|
2572
|
+
/** Template de rendu d'un jeton (chip) affiché sous le multi-select.
|
|
2573
|
+
Contexte : `let-option` = l'option sélectionnée (`T`), `let-remove` = fonction qui la retire.
|
|
2574
|
+
|
|
2575
|
+
```html
|
|
2576
|
+
<kt-multi-select [options]="tags">
|
|
2577
|
+
<ng-template [ktMultiSelectChip]="tags" let-tag let-remove="remove">
|
|
2578
|
+
<kt-chip [removable]="true" (remove)="remove()">{{ tag.name }}</kt-chip>
|
|
2579
|
+
</ng-template>
|
|
2580
|
+
</kt-multi-select>
|
|
2581
|
+
``` */
|
|
2582
|
+
class KtMultiSelectChipDef {
|
|
2583
|
+
ktMultiSelectChip = input.required(/* @ts-ignore */
|
|
2584
|
+
...(ngDevMode ? [{ debugName: "ktMultiSelectChip" }] : /* istanbul ignore next */ []));
|
|
2585
|
+
template = inject(TemplateRef);
|
|
2586
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
2587
|
+
return typeof ctx === 'object';
|
|
2588
|
+
}
|
|
2589
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectChipDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2590
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMultiSelectChipDef, isStandalone: true, selector: "ng-template[ktMultiSelectChip]", inputs: { ktMultiSelectChip: { classPropertyName: "ktMultiSelectChip", publicName: "ktMultiSelectChip", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
2591
|
+
}
|
|
2592
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelectChipDef, decorators: [{
|
|
2593
|
+
type: Directive,
|
|
2594
|
+
args: [{ selector: 'ng-template[ktMultiSelectChip]' }]
|
|
2595
|
+
}], propDecorators: { ktMultiSelectChip: [{ type: i0.Input, args: [{ isSignal: true, alias: "ktMultiSelectChip", required: true }] }] } });
|
|
2596
|
+
|
|
2597
|
+
// Défauts lib neutre (anglais, comme le Select single) : le consommateur fournit
|
|
2598
|
+
// ses textes via KT_SELECT_CONFIG / les inputs (pas de $localize — cf. select-config.ts).
|
|
2599
|
+
function defaultSelectionSummaryText(count) {
|
|
2600
|
+
return `${count} items selected`;
|
|
2601
|
+
}
|
|
2602
|
+
function defaultSelectionCountText(count) {
|
|
2603
|
+
return count === 1 ? '1 selected' : `${count} selected`;
|
|
2604
|
+
}
|
|
2605
|
+
function defaultSelectedItemsLabel(fieldLabel) {
|
|
2606
|
+
return fieldLabel ? `Selected items for ${fieldLabel}` : 'Selected items';
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Composant de sélection multiple (Multi-Select) conforme aux exigences d'accessibilité (WCAG 2.2 AAA / RGAA).
|
|
2610
|
+
* Repose sur `@angular/aria/combobox` et `@angular/aria/listbox` avec [multi]="true".
|
|
2611
|
+
* Présentation adaptative : dropdown (desktop) ou bottom-sheet modale (téléphone tactile).
|
|
2612
|
+
* Option 1 : Résumé textuel sur le trigger et chips révocables affichés en dessous.
|
|
2613
|
+
*
|
|
2614
|
+
* Clavier multi-sélection (APG) : Espace/Entrée toggle, Shift+flèches étend la sélection,
|
|
2615
|
+
* Ctrl+A tout (dé)sélectionne, Shift+Espace sélectionne la plage, typeahead — majoritairement
|
|
2616
|
+
* natifs via `@angular/aria` ; les combos non relayés par la lib le sont via `onTriggerKeydown`.
|
|
2617
|
+
* Tout le commun (popup, filtre, clavier, drag-to-dismiss) vit dans `BaseSelect`.
|
|
2618
|
+
*
|
|
2619
|
+
* @template T Type d'une option (élément du tableau `options`).
|
|
2620
|
+
* @template V Type des valeurs émises (défaut `T` ; clés extraites si `optionValue` est fourni).
|
|
2621
|
+
*
|
|
2622
|
+
* @example
|
|
2623
|
+
* ```html
|
|
2624
|
+
* <kt-multi-select [options]="tags" optionLabel="name" optionValue="id" [(value)]="selectedIds" />
|
|
2625
|
+
* ```
|
|
2626
|
+
*/
|
|
2627
|
+
class KtMultiSelect extends KtBaseSelect {
|
|
2628
|
+
// --- Modèle réactif ---
|
|
2629
|
+
/** Valeurs sélectionnées (clés extraites, ou objets entiers si `optionValue` est omis). @default [] */
|
|
2630
|
+
value = model([], /* @ts-ignore */
|
|
2631
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
2632
|
+
/** Émis à chaque (dé)sélection. Payload : `{ value, options }` — `value` le tableau des valeurs émises
|
|
2633
|
+
(clés ou objets), `options` les objets options correspondants, dans l'ordre de sélection. */
|
|
2634
|
+
selectionChange = output();
|
|
2635
|
+
// --- Présentation / Configuration (spécifique multi) ---
|
|
2636
|
+
/** Bouton « tout effacer » sur le champ (même contrat que `clearable` de BaseInputField). @default false */
|
|
2637
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2638
|
+
/** Libellé i18n du bouton « tout effacer ». @default 'Clear' (ou KT_FIELD_CONFIG.clearLabel) */
|
|
2639
|
+
clearLabel = input(this.fieldConfig?.clearLabel ?? 'Clear', /* @ts-ignore */
|
|
2640
|
+
...(ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []));
|
|
2641
|
+
/** Barre « Tout sélectionner / Tout effacer » en tête du popup (bascule le popup en panneau dialog). */
|
|
2642
|
+
selectionActions = input(false, { ...(ngDevMode ? { debugName: "selectionActions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2643
|
+
/** Nombre maximal de chips affichés avant repli derrière un bouton « +N more ». Défaut : illimité. */
|
|
2644
|
+
maxVisibleChips = input(/* @ts-ignore */
|
|
2645
|
+
...(ngDevMode ? [undefined, { debugName: "maxVisibleChips" }] : /* istanbul ignore next */ []));
|
|
2646
|
+
// --- Templates projetés ---
|
|
2647
|
+
optionDef = contentChild(KtMultiSelectOptionDef, /* @ts-ignore */
|
|
2648
|
+
...(ngDevMode ? [{ debugName: "optionDef" }] : /* istanbul ignore next */ []));
|
|
2649
|
+
triggerDef = contentChild(KtMultiSelectTriggerDef, /* @ts-ignore */
|
|
2650
|
+
...(ngDevMode ? [{ debugName: "triggerDef" }] : /* istanbul ignore next */ []));
|
|
2651
|
+
chipDef = contentChild(KtMultiSelectChipDef, /* @ts-ignore */
|
|
2652
|
+
...(ngDevMode ? [{ debugName: "chipDef" }] : /* istanbul ignore next */ []));
|
|
2653
|
+
chipList = viewChild(KtChipList, /* @ts-ignore */
|
|
2654
|
+
...(ngDevMode ? [{ debugName: "chipList" }] : /* istanbul ignore next */ []));
|
|
2655
|
+
// --- Textes résolus spécifiques multi (input > KT_SELECT_CONFIG > défaut lib neutre EN) ---
|
|
2656
|
+
resolvedRemoveItemLabel = computed(() => this.config?.removeItemLabel ?? ((itemLabel) => `Remove ${itemLabel}`), /* @ts-ignore */
|
|
2657
|
+
...(ngDevMode ? [{ debugName: "resolvedRemoveItemLabel" }] : /* istanbul ignore next */ []));
|
|
2658
|
+
resolvedSelectedItemsLabel = computed(() => (this.config?.selectedItemsLabel ?? defaultSelectedItemsLabel)(this.label()), /* @ts-ignore */
|
|
2659
|
+
...(ngDevMode ? [{ debugName: "resolvedSelectedItemsLabel" }] : /* istanbul ignore next */ []));
|
|
2660
|
+
resolvedSelectionSummaryText = computed(() => this.config?.selectionSummaryText ?? defaultSelectionSummaryText, /* @ts-ignore */
|
|
2661
|
+
...(ngDevMode ? [{ debugName: "resolvedSelectionSummaryText" }] : /* istanbul ignore next */ []));
|
|
2662
|
+
resolvedItemRemovedText = computed(() => this.config?.itemRemovedText ?? ((itemLabel) => `${itemLabel} removed`), /* @ts-ignore */
|
|
2663
|
+
...(ngDevMode ? [{ debugName: "resolvedItemRemovedText" }] : /* istanbul ignore next */ []));
|
|
2664
|
+
resolvedSelectionCountText = computed(() => this.config?.selectionCountText ?? defaultSelectionCountText, /* @ts-ignore */
|
|
2665
|
+
...(ngDevMode ? [{ debugName: "resolvedSelectionCountText" }] : /* istanbul ignore next */ []));
|
|
2666
|
+
resolvedSelectAllLabel = computed(() => this.config?.selectAllLabel ?? 'Select all', /* @ts-ignore */
|
|
2667
|
+
...(ngDevMode ? [{ debugName: "resolvedSelectAllLabel" }] : /* istanbul ignore next */ []));
|
|
2668
|
+
resolvedClearAllLabel = computed(() => this.config?.clearAllLabel ?? 'Clear all', /* @ts-ignore */
|
|
2669
|
+
...(ngDevMode ? [{ debugName: "resolvedClearAllLabel" }] : /* istanbul ignore next */ []));
|
|
2670
|
+
resolvedMoreChipsLabel = computed(() => this.config?.moreChipsLabel ?? ((hiddenCount) => `+${hiddenCount} more`), /* @ts-ignore */
|
|
2671
|
+
...(ngDevMode ? [{ debugName: "resolvedMoreChipsLabel" }] : /* istanbul ignore next */ []));
|
|
2672
|
+
resolvedLessChipsLabel = computed(() => this.config?.lessChipsLabel ?? 'Show less', /* @ts-ignore */
|
|
2673
|
+
...(ngDevMode ? [{ debugName: "resolvedLessChipsLabel" }] : /* istanbul ignore next */ []));
|
|
2674
|
+
/** Accesseurs bridgés vers le ChipList (fonctions fléchées : une référence de méthode
|
|
2675
|
+
non liée perdrait `this`). */
|
|
2676
|
+
chipLabelOf = (item) => this.labelOf(item);
|
|
2677
|
+
chipKeyOf = (item) => this.keyOf(item);
|
|
2678
|
+
/** Options correspondant aux entrées de `value`, dans l'ordre de sélection.
|
|
2679
|
+
Mode objet : chaque entrée est ramenée à l'option canonique équivalente (compareWith). */
|
|
2680
|
+
selectedOptions = computed(() => {
|
|
2681
|
+
const vals = this.value() ?? [];
|
|
2682
|
+
if (vals.length === 0)
|
|
2683
|
+
return [];
|
|
2684
|
+
const opts = this.options();
|
|
2685
|
+
if (this.optionValue() !== undefined) {
|
|
2686
|
+
return vals.map((v) => opts.find((o) => this.keyOf(o) === v)).filter((o) => o !== undefined);
|
|
2687
|
+
}
|
|
2688
|
+
const cmp = this.comparator();
|
|
2689
|
+
return vals.map((v) => opts.find((o) => cmp(o, v)) ?? v);
|
|
2690
|
+
}, /* @ts-ignore */
|
|
2691
|
+
...(ngDevMode ? [{ debugName: "selectedOptions" }] : /* istanbul ignore next */ []));
|
|
2692
|
+
// Texte synthétique du trigger (Option 1)
|
|
2693
|
+
triggerText = computed(() => {
|
|
2694
|
+
const selected = this.selectedOptions();
|
|
2695
|
+
if (selected.length === 0)
|
|
2696
|
+
return '';
|
|
2697
|
+
if (selected.length <= 3) {
|
|
2698
|
+
return selected.map((o) => this.labelOf(o)).join(', ');
|
|
2699
|
+
}
|
|
2700
|
+
return this.resolvedSelectionSummaryText()(selected.length);
|
|
2701
|
+
}, /* @ts-ignore */
|
|
2702
|
+
...(ngDevMode ? [{ debugName: "triggerText" }] : /* istanbul ignore next */ []));
|
|
2703
|
+
/** Valeur brute transmise au ngListbox sous-jacent (tableau de clés). */
|
|
2704
|
+
listboxValue = computed(() => this.selectedOptions().map((o) => this.keyOf(o)), /* @ts-ignore */
|
|
2705
|
+
...(ngDevMode ? [{ debugName: "listboxValue" }] : /* istanbul ignore next */ []));
|
|
2706
|
+
// --- Tout effacer depuis le champ (clearable) ---
|
|
2707
|
+
showClear = computed(() => this.clearable() && !this.disabled() && !this.readonly() && this.selectedOptions().length > 0, /* @ts-ignore */
|
|
2708
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
2709
|
+
/** Le popup utilise la structure « panneau dialog » (champ de filtre et/ou barre d'actions). */
|
|
2710
|
+
dialogMode = computed(() => this.filterable() || this.selectionActions(), /* @ts-ignore */
|
|
2711
|
+
...(ngDevMode ? [{ debugName: "dialogMode" }] : /* istanbul ignore next */ []));
|
|
2712
|
+
/** Compteur de sélection affiché dans le panneau (info déjà portée par la live region → aria-hidden). */
|
|
2713
|
+
selectionCountLabel = computed(() => this.resolvedSelectionCountText()(this.selectedOptions().length), /* @ts-ignore */
|
|
2714
|
+
...(ngDevMode ? [{ debugName: "selectionCountLabel" }] : /* istanbul ignore next */ []));
|
|
2715
|
+
/** Clé listbox d'une entrée de `value` (mode objet : clé de l'option canonique équivalente). */
|
|
2716
|
+
entryKey(v) {
|
|
2717
|
+
if (this.optionValue() !== undefined)
|
|
2718
|
+
return v;
|
|
2719
|
+
const cmp = this.comparator();
|
|
2720
|
+
const option = this.options().find((o) => cmp(o, v));
|
|
2721
|
+
return option !== undefined ? this.keyOf(option) : this.keyOf(v);
|
|
2722
|
+
}
|
|
2723
|
+
/** Entrée de `value` à committer pour une clé listbox (mode objet : l'option entière). */
|
|
2724
|
+
entryForKey(key) {
|
|
2725
|
+
const option = this.options().find((o) => this.keyOf(o) === key);
|
|
2726
|
+
if (option === undefined)
|
|
2727
|
+
return undefined;
|
|
2728
|
+
return this.optionValue() !== undefined ? key : option;
|
|
2729
|
+
}
|
|
2730
|
+
onListboxValueChange(keys) {
|
|
2731
|
+
// Le listbox ne connaît que les options RENDUES : quand le filtre en masque, sa purge
|
|
2732
|
+
// interne retire leurs clés (pas un geste utilisateur). On conserve donc les entrées
|
|
2733
|
+
// hors vue, on applique les (dé)sélections visibles dans l'ordre courant, et on ignore
|
|
2734
|
+
// l'événement si rien n'a réellement changé (sinon touched/selectionChange parasites).
|
|
2735
|
+
const current = this.value() ?? [];
|
|
2736
|
+
const visibleKeys = new Set(this.displayedOptions().map((o) => this.keyOf(o)));
|
|
2737
|
+
const keptKeys = new Set(keys);
|
|
2738
|
+
const kept = current.filter((v) => {
|
|
2739
|
+
const k = this.entryKey(v);
|
|
2740
|
+
return !visibleKeys.has(k) || keptKeys.has(k);
|
|
2741
|
+
});
|
|
2742
|
+
const currentKeys = new Set(current.map((v) => this.entryKey(v)));
|
|
2743
|
+
const added = keys
|
|
2744
|
+
.filter((k) => !currentKeys.has(k))
|
|
2745
|
+
.map((k) => this.entryForKey(k))
|
|
2746
|
+
.filter((v) => v !== undefined);
|
|
2747
|
+
if (kept.length === current.length && added.length === 0)
|
|
2748
|
+
return;
|
|
2749
|
+
this.commitValue([...kept, ...added]);
|
|
2750
|
+
}
|
|
2751
|
+
commitValue(next) {
|
|
2752
|
+
this.value.set(next);
|
|
2753
|
+
this.touched.set(true);
|
|
2754
|
+
this.selectionChange.emit({ value: next, options: this.selectedOptions() });
|
|
2755
|
+
}
|
|
2756
|
+
/** Comble les combos que le relay du combobox ne transmet pas en activedescendant :
|
|
2757
|
+
Shift+Espace (sélection de plage) et Ctrl/Cmd+Shift+Home/End (plage jusqu'aux bornes). */
|
|
2758
|
+
onTriggerKeydown(event) {
|
|
2759
|
+
if (!this.expanded() || this.compact())
|
|
2760
|
+
return;
|
|
2761
|
+
const shiftSpace = event.key === ' ' && event.shiftKey && !event.ctrlKey && !event.metaKey;
|
|
2762
|
+
const rangeToEdge = (event.key === 'Home' || event.key === 'End') && event.shiftKey && (event.ctrlKey || event.metaKey);
|
|
2763
|
+
if (!shiftSpace && !rangeToEdge)
|
|
2764
|
+
return;
|
|
2765
|
+
const listbox = this.listboxEl()?.nativeElement;
|
|
2766
|
+
if (!listbox)
|
|
2767
|
+
return;
|
|
2768
|
+
event.preventDefault();
|
|
2769
|
+
listbox.dispatchEvent(new KeyboardEvent('keydown', {
|
|
2770
|
+
key: event.key,
|
|
2771
|
+
shiftKey: event.shiftKey,
|
|
2772
|
+
ctrlKey: event.ctrlKey,
|
|
2773
|
+
metaKey: event.metaKey,
|
|
2774
|
+
cancelable: true,
|
|
2775
|
+
}));
|
|
2776
|
+
}
|
|
2777
|
+
/** En panneau dialog, le relay du combobox vise le widget (la div), pas le listbox descendant :
|
|
2778
|
+
on fait suivre au listbox les événements relayés (target = la div elle-même uniquement —
|
|
2779
|
+
les keydown des éléments internes, qui bubblent, ne sont pas re-forwardés). */
|
|
2780
|
+
onPanelKeydown(event) {
|
|
2781
|
+
if (event.target !== event.currentTarget)
|
|
2782
|
+
return;
|
|
2783
|
+
const listbox = this.listboxEl()?.nativeElement;
|
|
2784
|
+
if (!listbox)
|
|
2785
|
+
return;
|
|
2786
|
+
listbox.dispatchEvent(new KeyboardEvent('keydown', {
|
|
2787
|
+
key: event.key,
|
|
2788
|
+
shiftKey: event.shiftKey,
|
|
2789
|
+
ctrlKey: event.ctrlKey,
|
|
2790
|
+
metaKey: event.metaKey,
|
|
2791
|
+
cancelable: true,
|
|
2792
|
+
}));
|
|
2793
|
+
}
|
|
2794
|
+
/** « N results » + « , M selected » quand des sélections existent (dont celles hors filtre). */
|
|
2795
|
+
composeFilterAnnouncement() {
|
|
2796
|
+
const base = super.composeFilterAnnouncement();
|
|
2797
|
+
if (base === '')
|
|
2798
|
+
return '';
|
|
2799
|
+
const selectedCount = this.selectedOptions().length;
|
|
2800
|
+
if (selectedCount === 0)
|
|
2801
|
+
return base;
|
|
2802
|
+
return `${base}, ${this.resolvedSelectionCountText()(selectedCount)}`;
|
|
2803
|
+
}
|
|
2804
|
+
/** Retire une option de la valeur (annonce et focus : délégués au ChipList). */
|
|
2805
|
+
removeOption(option) {
|
|
2806
|
+
if (this.disabled() || this.readonly())
|
|
2807
|
+
return;
|
|
2808
|
+
const current = this.value() ?? [];
|
|
2809
|
+
const next = this.removeFromValue(option);
|
|
2810
|
+
if (next.length === current.length)
|
|
2811
|
+
return;
|
|
2812
|
+
this.commitValue(next);
|
|
2813
|
+
}
|
|
2814
|
+
removeFromValue(option) {
|
|
2815
|
+
const current = this.value() ?? [];
|
|
2816
|
+
if (this.optionValue() !== undefined) {
|
|
2817
|
+
const key = this.keyOf(option);
|
|
2818
|
+
return current.filter((v) => v !== key);
|
|
2819
|
+
}
|
|
2820
|
+
const cmp = this.comparator();
|
|
2821
|
+
return current.filter((v) => !cmp(v, option));
|
|
2822
|
+
}
|
|
2823
|
+
/** Vide toute la sélection depuis le champ (bouton `clearable`). */
|
|
2824
|
+
clearSelection() {
|
|
2825
|
+
if (!this.showClear())
|
|
2826
|
+
return;
|
|
2827
|
+
this.commitValue([]);
|
|
2828
|
+
this.chipList()?.announce(this.resolvedSelectionCountText()(0));
|
|
2829
|
+
this.triggerEl()?.nativeElement.focus();
|
|
2830
|
+
}
|
|
2831
|
+
/** Sélectionne toutes les options visibles non désactivées (sémantique du Ctrl+A natif :
|
|
2832
|
+
avec un filtre actif, seules les options filtrées sont concernées). */
|
|
2833
|
+
selectAllFiltered() {
|
|
2834
|
+
if (this.disabled() || this.readonly())
|
|
2835
|
+
return;
|
|
2836
|
+
const current = this.value() ?? [];
|
|
2837
|
+
const currentKeys = new Set(current.map((v) => this.entryKey(v)));
|
|
2838
|
+
const added = this.filteredOptions()
|
|
2839
|
+
.filter((o) => !this.disabledOf(o) && !currentKeys.has(this.keyOf(o)))
|
|
2840
|
+
.map((o) => (this.optionValue() !== undefined ? this.keyOf(o) : o));
|
|
2841
|
+
if (added.length === 0)
|
|
2842
|
+
return;
|
|
2843
|
+
this.commitValue([...current, ...added]);
|
|
2844
|
+
this.announceSelectionCount();
|
|
2845
|
+
}
|
|
2846
|
+
/** Désélectionne toutes les options visibles (les sélections masquées par le filtre restent). */
|
|
2847
|
+
clearAllFiltered() {
|
|
2848
|
+
if (this.disabled() || this.readonly())
|
|
2849
|
+
return;
|
|
2850
|
+
const current = this.value() ?? [];
|
|
2851
|
+
const visibleKeys = new Set(this.filteredOptions().map((o) => this.keyOf(o)));
|
|
2852
|
+
const next = current.filter((v) => !visibleKeys.has(this.entryKey(v)));
|
|
2853
|
+
if (next.length === current.length)
|
|
2854
|
+
return;
|
|
2855
|
+
this.commitValue(next);
|
|
2856
|
+
this.announceSelectionCount();
|
|
2857
|
+
}
|
|
2858
|
+
/** Annonce immédiate (action ponctuelle, pas de débounce) du total sélectionné. */
|
|
2859
|
+
announceSelectionCount() {
|
|
2860
|
+
this.announceNow(this.resolvedSelectionCountText()(this.selectedOptions().length));
|
|
2861
|
+
}
|
|
2862
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelect, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
2863
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtMultiSelect, isStandalone: true, selector: "kt-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, selectionActions: { classPropertyName: "selectionActions", publicName: "selectionActions", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleChips: { classPropertyName: "maxVisibleChips", publicName: "maxVisibleChips", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", selectionChange: "selectionChange" }, queries: [{ propertyName: "optionDef", first: true, predicate: KtMultiSelectOptionDef, descendants: true, isSignal: true }, { propertyName: "triggerDef", first: true, predicate: KtMultiSelectTriggerDef, descendants: true, isSignal: true }, { propertyName: "chipDef", first: true, predicate: KtMultiSelectChipDef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "chipList", first: true, predicate: KtChipList, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"fieldErrors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div class=\"kt-select\" [class.kt-select--open]=\"expanded()\">\n <button\n #combobox=\"ngCombobox\"\n #trigger\n ngCombobox\n ktFieldControl\n type=\"button\"\n class=\"kt-field-box kt-select__trigger\"\n [class.kt-select__trigger--clearable]=\"showClear()\"\n [(expanded)]=\"expanded\"\n [disabled]=\"disabled()\"\n [softDisabled]=\"false\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"kt-select__value\">\n @if (triggerDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedOptions() }\"\n />\n } @else if (selectedOptions().length > 0) {\n {{ triggerText() }}\n } @else {\n <span class=\"kt-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n </span>\n <span class=\"kt-select__arrow\" aria-hidden=\"true\">arrow_drop_down</span>\n </button>\n\n <!-- Tout effacer depuis le champ : sibling du trigger (pas de bouton imbriqu\u00E9 dans un bouton). -->\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"kt-select__clear\"\n [attr.aria-label]=\"clearLabel()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"clearSelection()\"\n >\n <span class=\"kt-select__clear-icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n <!-- Popup Popover (top-layer). M\u00EAme logique mobile bottom-sheet adaptative que le select classique. -->\n <ng-template ngComboboxPopup [combobox]=\"combobox\" [popupType]=\"dialogMode() ? 'dialog' : 'listbox'\">\n <div #popup popover=\"manual\" class=\"kt-select__popup\" [class.kt-select__popup--sheet]=\"compact()\">\n @if (compact()) {\n <div class=\"kt-select__sheet-scrim\" aria-hidden=\"true\" (click)=\"expanded.set(false)\"></div>\n }\n <div class=\"kt-select__sheet-card\">\n @if (compact()) {\n <div\n class=\"kt-select__sheet-grab\"\n aria-hidden=\"true\"\n (pointerdown)=\"onDragStart($event)\"\n (mousedown)=\"$event.preventDefault()\"\n ></div>\n <header class=\"kt-select__sheet-header\">\n <span class=\"kt-select__sheet-title\">{{ label() }}</span>\n <button\n type=\"button\"\n class=\"kt-select__sheet-close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"expanded.set(false)\"\n >\n <span class=\"kt-select__sheet-close-icon\" aria-hidden=\"true\">close</span>\n </button>\n </header>\n }\n @if (dialogMode()) {\n <!-- Mode panneau (filtre et/ou actions de masse) : Widget Dialog -->\n <div\n ngComboboxWidget\n role=\"dialog\"\n class=\"kt-select__panel\"\n [id]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [activeDescendant]=\"lb.activeDescendant()\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @if (selectionActions()) {\n <div class=\"kt-select__actions\">\n <button type=\"button\" class=\"kt-select__action\" (click)=\"selectAllFiltered()\">\n {{ resolvedSelectAllLabel() }}\n </button>\n <button type=\"button\" class=\"kt-select__action\" (click)=\"clearAllFiltered()\">\n {{ resolvedClearAllLabel() }}\n </button>\n </div>\n }\n @if (filterable()) {\n <div class=\"kt-select__filter\">\n <input\n #filterInput\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n class=\"kt-select__filter-input\"\n autocomplete=\"off\"\n [placeholder]=\"resolvedFilterPlaceholder()\"\n [attr.aria-label]=\"resolvedFilterLabel()\"\n [attr.aria-controls]=\"lb.id()\"\n [attr.aria-activedescendant]=\"lb.activeDescendant()\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeydown($event)\"\n />\n </div>\n }\n @if (selectedOptions().length > 0) {\n <!-- Rappel visuel (s\u00E9lections potentiellement masqu\u00E9es par le filtre) ;\n l'info SR passe par la live region ci-dessous \u2192 aria-hidden. -->\n <div class=\"kt-select__count\" aria-hidden=\"true\">{{ selectionCountLabel() }}</div>\n }\n <div class=\"kt-select__sr-only kt-select__filter-status\" role=\"status\" aria-live=\"polite\">\n {{ announcedCount() }}\n </div>\n <ul\n #lb=\"ngListbox\"\n #listboxEl\n ngListbox\n [multi]=\"true\"\n [focusMode]=\"compact() && !filterable() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n >\n @for (item of displayedOptions(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option kt-select__option--multiple\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n <span class=\"kt-select__checkbox\" aria-hidden=\"true\">\n @if (opt.selected()) {\n <span class=\"kt-select__checkbox-icon\">check_box</span>\n } @else {\n <span class=\"kt-select__checkbox-icon\">check_box_outline_blank</span>\n }\n </span>\n <span class=\"kt-select__option-content\">\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </span>\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n @if (filterable() && filteredOptions().length > maxVisibleOptions()) {\n <li class=\"kt-select__truncated-info\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedTruncatedResultsText()(maxVisibleOptions(), filteredOptions().length) }}\n </li>\n }\n </ul>\n </div>\n } @else {\n <!-- Mode simple : Widget Listbox direct -->\n <ul\n #listbox=\"ngListbox\"\n #listboxEl\n ngComboboxWidget\n ngListbox\n [multi]=\"true\"\n [focusMode]=\"compact() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [activeDescendant]=\"listbox.activeDescendant()\"\n >\n @for (item of options(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option kt-select__option--multiple\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n <span class=\"kt-select__checkbox\" aria-hidden=\"true\">\n @if (opt.selected()) {\n <span class=\"kt-select__checkbox-icon\">check_box</span>\n } @else {\n <span class=\"kt-select__checkbox-icon\">check_box_outline_blank</span>\n }\n </span>\n <span class=\"kt-select__option-content\">\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </span>\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n </ul>\n }\n </div>\n </div>\n </ng-template>\n </div>\n\n <!-- Chips r\u00E9vocables (Option 1) sous le trigger : d\u00E9l\u00E9gu\u00E9s au composant r\u00E9utilisable\n kt-chip-list (focus management, repli +N, live region). Les textes r\u00E9solus depuis\n KT_SELECT_CONFIG sont pass\u00E9s en inputs (ils priment sur un \u00E9ventuel KT_CHIPS_CONFIG). -->\n <kt-chip-list\n [items]=\"selectedOptions()\"\n [itemLabel]=\"chipLabelOf\"\n [itemKey]=\"chipKeyOf\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [maxVisible]=\"maxVisibleChips()\"\n [listLabel]=\"resolvedSelectedItemsLabel()\"\n [removeItemLabel]=\"resolvedRemoveItemLabel()\"\n [itemRemovedText]=\"resolvedItemRemovedText()\"\n [moreLabel]=\"resolvedMoreChipsLabel()\"\n [lessLabel]=\"resolvedLessChipsLabel()\"\n [chipTemplate]=\"chipDef()?.template ?? null\"\n [emptyFocusTarget]=\"triggerEl()?.nativeElement\"\n (removed)=\"removeOption($event.item)\"\n />\n</kt-field>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-select{position:relative}.kt-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--field-control-gap, .5rem);inline-size:100%;cursor:pointer;text-align:start;font:inherit;color:var(--field-color, inherit)}.kt-select__trigger:disabled{cursor:not-allowed;background:var(--field-disabled-bg, #f1f3f4);color:color-mix(in srgb,currentColor 50%,transparent)}.kt-select__value{flex:1;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-select__placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__arrow{flex:none;font-family:Material Symbols Outlined;font-size:1.25em;line-height:1;font-feature-settings:\"liga\";color:var(--field-icon-color, #5f6368);transition:var(--select-arrow-transition, transform .12s ease);-webkit-font-smoothing:antialiased}.kt-select--open .kt-select__arrow{transform:rotate(180deg)}.kt-select__popup{box-sizing:border-box;display:flex;flex-direction:column;margin:0;padding:0;border-width:var(--select-popup-border-width, var(--field-border-width, 1px));border-style:var(--field-border-style, solid);border-color:var(--field-border-color, #c4c7c5);border-radius:var(--field-radius, 8px);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);box-shadow:var(--select-popup-shadow, 0 4px 12px rgb(0 0 0 / 12%));-webkit-backdrop-filter:var(--select-popup-backdrop-filter, none);backdrop-filter:var(--select-popup-backdrop-filter, none);max-block-size:var(--select-popup-max-height, 16rem);overflow:hidden}.kt-select__sheet-card{display:contents}.kt-select__popup:not(:popover-open){display:none!important}.kt-select__popup:popover-open{animation:var(--select-popup-enter-animation, none)}@media(prefers-reduced-motion:reduce){.kt-select__popup:popover-open{animation:none}}.kt-select__listbox{flex:1 1 auto;min-block-size:0;margin:0;padding:.25rem;list-style:none;overflow-y:auto}.kt-select__panel{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0}.kt-select__filter{flex:none;padding:.5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__filter-input{box-sizing:border-box;inline-size:100%;min-block-size:var(--field-min-height, 44px);padding:.375rem .625rem;border:var(--field-border-width, 1px) var(--field-border-style, solid) var(--field-border-color, #c4c7c5);border-radius:calc(var(--field-radius, 8px) * .75);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);font:inherit;appearance:none}.kt-select__filter-input::placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__filter-input:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-1px}.kt-select__filter-input::-webkit-search-cancel-button{appearance:none;inline-size:1rem;block-size:1rem;margin-inline-start:.375rem;cursor:pointer;background-color:var(--field-icon-color, #5f6368);-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat;mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat}.kt-select__sr-only{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@supports (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto;top:anchor(bottom);left:anchor(left);margin-block-start:.25rem;min-inline-size:anchor-size(width);width:max-content;max-inline-size:min(90vw,28rem);position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline}}@supports not (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto 0 0;inline-size:100%;max-inline-size:100%;border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0}}.kt-select__popup--sheet{position:fixed!important;inset:0!important;inline-size:100%!important;max-inline-size:100%!important;min-inline-size:0!important;width:100%!important;height:100%!important;max-block-size:none!important;margin:0!important;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important;border-radius:0!important;overflow:visible!important;display:flex;flex-direction:column!important;justify-content:flex-end!important}.kt-select__popup--sheet .kt-select__sheet-card{position:relative;z-index:2;display:flex;flex-direction:column;background:var(--select-popup-bg, var(--kt-surface, #fff));border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0;box-shadow:var(--kt-sheet-shadow, 0 -4px 16px rgb(0 0 0 / 12%));max-block-size:var(--kt-sheet-max-block-size, 85svh);width:100%;overflow:hidden;translate:0 0;transition:translate var(--kt-sheet-anim-duration, .12s) ease}.kt-select__popup--sheet .kt-select__sheet-card:has(.kt-select__filter){block-size:var(--kt-sheet-max-block-size, 85svh);max-block-size:var(--kt-sheet-max-block-size, 85svh)}.kt-select__popup--sheet:popover-open .kt-select__sheet-card{translate:0 0;animation:var(--select-sheet-enter-animation, kt-sheet-in var(--kt-sheet-anim-duration, .12s) ease)}.kt-select__popup--sheet .kt-select__sheet-card.kt-select__popup--dragging{transition:none}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-card,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-card{translate:0 100%;transition:translate var(--kt-sheet-exit-duration, 90ms) cubic-bezier(.4,0,.2,1)}::ng-deep .kt-select__popup--sheet::backdrop{display:none!important}.kt-select__sheet-scrim{display:none}.kt-select__popup--sheet .kt-select__sheet-scrim{display:block;position:fixed;inset:0;z-index:1;background:var(--kt-sheet-scrim, rgb(0 0 0 / 40%));opacity:0;pointer-events:auto;transition:opacity var(--kt-sheet-anim-duration, .12s) ease,overlay var(--kt-sheet-anim-duration, .12s) allow-discrete,display var(--kt-sheet-anim-duration, .12s) allow-discrete}.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:1}@starting-style{.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:0}}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-scrim,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-scrim{opacity:0;transition:opacity var(--kt-sheet-exit-duration, 90ms) ease}.kt-select__popup--sheet .kt-select__option{--select-option-min-height: 44px;padding-block:.625rem}.kt-select__popup--sheet .kt-select__filter-input{font-size:1rem;min-block-size:44px}@media(prefers-reduced-motion:reduce){.kt-select__popup--sheet .kt-select__sheet-card,.kt-select__popup--sheet .kt-select__sheet-scrim{transition:none;translate:0 0}}.kt-select__sheet-grab{display:flex;align-items:center;justify-content:center;flex:none;block-size:44px;cursor:grab;touch-action:none}.kt-select__sheet-grab:active{cursor:grabbing}.kt-select__sheet-grab:before{content:\"\";inline-size:2.25rem;block-size:.25rem;border-radius:999px;background:var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5))}.kt-select__sheet-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex:none;padding-block:.5rem;padding-inline:1rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__sheet-title{font-weight:600}.kt-select__sheet-close{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:44px;block-size:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__sheet-close:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:2px}.kt-select__sheet-close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.5rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__option{display:flex;align-items:center;gap:.5rem;box-sizing:border-box;min-block-size:var( --select-option-min-height, 44px );padding:.5rem .625rem;border-radius:6px;cursor:pointer}.kt-select__option[aria-selected=true]{background:var(--select-option-selected-bg, color-mix(in srgb, var(--kt-primary, #0b57d0) 14%, transparent));color:var(--select-option-selected-color, inherit);font-weight:var(--select-option-selected-weight, 600)}.kt-select__option--active:not([aria-disabled=true]),.kt-select__option:hover:not([aria-disabled=true]){background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__option[aria-disabled=true]{opacity:.5;cursor:not-allowed}.kt-select__empty{padding:.5rem .625rem;color:var(--field-hint-color, #5f6368)}.kt-select__truncated-info{box-sizing:border-box;padding:.5rem .625rem;font-size:.875rem;font-style:italic;color:var(--field-hint-color, #5f6368);border-block-start:1px solid var(--field-border-color, #c4c7c5);pointer-events:none}}\n", "@layer kt-aaa.components{.kt-select__trigger--clearable .kt-select__value{margin-inline-end:2rem}.kt-select__clear{position:absolute;inset-block-start:50%;inset-inline-end:2.5rem;translate:0 -50%;display:inline-flex;align-items:center;justify-content:center;inline-size:24px;block-size:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__clear:after{content:\"\";position:absolute;inset:-10px}.kt-select__clear:hover{background-color:color-mix(in srgb,currentColor 12%,transparent);color:var(--field-color, inherit)}.kt-select__clear:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:1px}.kt-select__clear-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.25rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__actions{display:flex;gap:.25rem;flex:none;padding:.25rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__action{flex:1;min-block-size:44px;padding:.375rem .625rem;border:0;border-radius:calc(var(--field-radius, 8px) * .75);background:transparent;color:var(--kt-primary, #0b57d0);font:inherit;font-weight:500;cursor:pointer}.kt-select__action:hover{background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__action:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-2px}.kt-select__count{flex:none;padding:.25rem .75rem;font-size:.8125rem;color:var(--field-hint-color, #5f6368);border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__checkbox{display:inline-flex;align-items:center;justify-content:center;flex:none;margin-inline-end:.25rem;-webkit-user-select:none;user-select:none}.kt-select__checkbox-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.25rem;line-height:1;color:var(--field-icon-color, #5f6368);-webkit-font-smoothing:antialiased}.kt-select__option[aria-selected=true] .kt-select__checkbox-icon{color:var(--kt-primary, #0b57d0)}.kt-select__option--multiple{display:flex;align-items:center;gap:.5rem}.kt-select__option-content{flex:1;min-inline-size:0}kt-chip-list:not([data-empty]){margin-block-start:.5rem}}\n"], dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: Combobox, selector: "[ngCombobox]", inputs: ["disabled", "softDisabled", "alwaysExpanded", "tabindex", "expanded", "value", "inlineSuggestion"], outputs: ["expandedChange", "valueChange"], exportAs: ["ngCombobox"] }, { kind: "directive", type: ComboboxPopup, selector: "ng-template[ngComboboxPopup]", inputs: ["combobox", "popupType"], exportAs: ["ngComboboxPopup"] }, { kind: "directive", type: ComboboxWidget, selector: "[ngComboboxWidget]", inputs: ["activeDescendant"], exportAs: ["ngComboboxWidget"] }, { kind: "directive", type: Listbox, selector: "[ngListbox]", inputs: ["id", "orientation", "multi", "wrap", "softDisabled", "focusMode", "selectionMode", "typeaheadDelay", "disabled", "readonly", "tabindex", "value"], outputs: ["valueChange"], exportAs: ["ngListbox"] }, { kind: "directive", type: Option, selector: "[ngOption]", inputs: ["id", "value", "disabled", "label"], exportAs: ["ngOption"] }, { kind: "component", type: KtChipList, selector: "kt-chip-list", inputs: ["items", "itemLabel", "itemKey", "removable", "disabled", "readonly", "maxVisible", "listLabel", "removeItemLabel", "itemRemovedText", "moreLabel", "lessLabel", "chipTemplate", "emptyFocusTarget"], outputs: ["removed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2864
|
+
}
|
|
2865
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMultiSelect, decorators: [{
|
|
2866
|
+
type: Component,
|
|
2867
|
+
args: [{ selector: 'kt-multi-select', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
2868
|
+
KtField,
|
|
2869
|
+
KtFieldControl,
|
|
2870
|
+
NgTemplateOutlet,
|
|
2871
|
+
Combobox,
|
|
2872
|
+
ComboboxPopup,
|
|
2873
|
+
ComboboxWidget,
|
|
2874
|
+
Listbox,
|
|
2875
|
+
Option,
|
|
2876
|
+
KtChipList,
|
|
2877
|
+
], template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"fieldErrors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div class=\"kt-select\" [class.kt-select--open]=\"expanded()\">\n <button\n #combobox=\"ngCombobox\"\n #trigger\n ngCombobox\n ktFieldControl\n type=\"button\"\n class=\"kt-field-box kt-select__trigger\"\n [class.kt-select__trigger--clearable]=\"showClear()\"\n [(expanded)]=\"expanded\"\n [disabled]=\"disabled()\"\n [softDisabled]=\"false\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"kt-select__value\">\n @if (triggerDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedOptions() }\"\n />\n } @else if (selectedOptions().length > 0) {\n {{ triggerText() }}\n } @else {\n <span class=\"kt-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n </span>\n <span class=\"kt-select__arrow\" aria-hidden=\"true\">arrow_drop_down</span>\n </button>\n\n <!-- Tout effacer depuis le champ : sibling du trigger (pas de bouton imbriqu\u00E9 dans un bouton). -->\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"kt-select__clear\"\n [attr.aria-label]=\"clearLabel()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"clearSelection()\"\n >\n <span class=\"kt-select__clear-icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n <!-- Popup Popover (top-layer). M\u00EAme logique mobile bottom-sheet adaptative que le select classique. -->\n <ng-template ngComboboxPopup [combobox]=\"combobox\" [popupType]=\"dialogMode() ? 'dialog' : 'listbox'\">\n <div #popup popover=\"manual\" class=\"kt-select__popup\" [class.kt-select__popup--sheet]=\"compact()\">\n @if (compact()) {\n <div class=\"kt-select__sheet-scrim\" aria-hidden=\"true\" (click)=\"expanded.set(false)\"></div>\n }\n <div class=\"kt-select__sheet-card\">\n @if (compact()) {\n <div\n class=\"kt-select__sheet-grab\"\n aria-hidden=\"true\"\n (pointerdown)=\"onDragStart($event)\"\n (mousedown)=\"$event.preventDefault()\"\n ></div>\n <header class=\"kt-select__sheet-header\">\n <span class=\"kt-select__sheet-title\">{{ label() }}</span>\n <button\n type=\"button\"\n class=\"kt-select__sheet-close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"expanded.set(false)\"\n >\n <span class=\"kt-select__sheet-close-icon\" aria-hidden=\"true\">close</span>\n </button>\n </header>\n }\n @if (dialogMode()) {\n <!-- Mode panneau (filtre et/ou actions de masse) : Widget Dialog -->\n <div\n ngComboboxWidget\n role=\"dialog\"\n class=\"kt-select__panel\"\n [id]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [activeDescendant]=\"lb.activeDescendant()\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @if (selectionActions()) {\n <div class=\"kt-select__actions\">\n <button type=\"button\" class=\"kt-select__action\" (click)=\"selectAllFiltered()\">\n {{ resolvedSelectAllLabel() }}\n </button>\n <button type=\"button\" class=\"kt-select__action\" (click)=\"clearAllFiltered()\">\n {{ resolvedClearAllLabel() }}\n </button>\n </div>\n }\n @if (filterable()) {\n <div class=\"kt-select__filter\">\n <input\n #filterInput\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n class=\"kt-select__filter-input\"\n autocomplete=\"off\"\n [placeholder]=\"resolvedFilterPlaceholder()\"\n [attr.aria-label]=\"resolvedFilterLabel()\"\n [attr.aria-controls]=\"lb.id()\"\n [attr.aria-activedescendant]=\"lb.activeDescendant()\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeydown($event)\"\n />\n </div>\n }\n @if (selectedOptions().length > 0) {\n <!-- Rappel visuel (s\u00E9lections potentiellement masqu\u00E9es par le filtre) ;\n l'info SR passe par la live region ci-dessous \u2192 aria-hidden. -->\n <div class=\"kt-select__count\" aria-hidden=\"true\">{{ selectionCountLabel() }}</div>\n }\n <div class=\"kt-select__sr-only kt-select__filter-status\" role=\"status\" aria-live=\"polite\">\n {{ announcedCount() }}\n </div>\n <ul\n #lb=\"ngListbox\"\n #listboxEl\n ngListbox\n [multi]=\"true\"\n [focusMode]=\"compact() && !filterable() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n >\n @for (item of displayedOptions(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option kt-select__option--multiple\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n <span class=\"kt-select__checkbox\" aria-hidden=\"true\">\n @if (opt.selected()) {\n <span class=\"kt-select__checkbox-icon\">check_box</span>\n } @else {\n <span class=\"kt-select__checkbox-icon\">check_box_outline_blank</span>\n }\n </span>\n <span class=\"kt-select__option-content\">\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </span>\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n @if (filterable() && filteredOptions().length > maxVisibleOptions()) {\n <li class=\"kt-select__truncated-info\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedTruncatedResultsText()(maxVisibleOptions(), filteredOptions().length) }}\n </li>\n }\n </ul>\n </div>\n } @else {\n <!-- Mode simple : Widget Listbox direct -->\n <ul\n #listbox=\"ngListbox\"\n #listboxEl\n ngComboboxWidget\n ngListbox\n [multi]=\"true\"\n [focusMode]=\"compact() ? 'roving' : 'activedescendant'\"\n selectionMode=\"explicit\"\n class=\"kt-select__listbox\"\n [attr.aria-label]=\"label()\"\n [value]=\"listboxValue()\"\n (valueChange)=\"onListboxValueChange($event)\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [activeDescendant]=\"listbox.activeDescendant()\"\n >\n @for (item of options(); track keyOf(item)) {\n <li\n #opt=\"ngOption\"\n ngOption\n class=\"kt-select__option kt-select__option--multiple\"\n [value]=\"keyOf(item)\"\n [label]=\"labelOf(item)\"\n [disabled]=\"disabledOf(item)\"\n [class.kt-select__option--active]=\"opt.active()\"\n >\n <span class=\"kt-select__checkbox\" aria-hidden=\"true\">\n @if (opt.selected()) {\n <span class=\"kt-select__checkbox-icon\">check_box</span>\n } @else {\n <span class=\"kt-select__checkbox-icon\">check_box_outline_blank</span>\n }\n </span>\n <span class=\"kt-select__option-content\">\n @if (optionDef(); as def) {\n <ng-container\n [ngTemplateOutlet]=\"def.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n selected: !!opt.selected(),\n active: opt.active(),\n }\"\n />\n } @else {\n {{ labelOf(item) }}\n }\n </span>\n </li>\n } @empty {\n <li class=\"kt-select__empty\" role=\"option\" aria-disabled=\"true\" aria-selected=\"false\">\n {{ resolvedEmptyText() }}\n </li>\n }\n </ul>\n }\n </div>\n </div>\n </ng-template>\n </div>\n\n <!-- Chips r\u00E9vocables (Option 1) sous le trigger : d\u00E9l\u00E9gu\u00E9s au composant r\u00E9utilisable\n kt-chip-list (focus management, repli +N, live region). Les textes r\u00E9solus depuis\n KT_SELECT_CONFIG sont pass\u00E9s en inputs (ils priment sur un \u00E9ventuel KT_CHIPS_CONFIG). -->\n <kt-chip-list\n [items]=\"selectedOptions()\"\n [itemLabel]=\"chipLabelOf\"\n [itemKey]=\"chipKeyOf\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [maxVisible]=\"maxVisibleChips()\"\n [listLabel]=\"resolvedSelectedItemsLabel()\"\n [removeItemLabel]=\"resolvedRemoveItemLabel()\"\n [itemRemovedText]=\"resolvedItemRemovedText()\"\n [moreLabel]=\"resolvedMoreChipsLabel()\"\n [lessLabel]=\"resolvedLessChipsLabel()\"\n [chipTemplate]=\"chipDef()?.template ?? null\"\n [emptyFocusTarget]=\"triggerEl()?.nativeElement\"\n (removed)=\"removeOption($event.item)\"\n />\n</kt-field>\n", styles: ["@layer kt-aaa.components{:host{display:block}.kt-select{position:relative}.kt-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--field-control-gap, .5rem);inline-size:100%;cursor:pointer;text-align:start;font:inherit;color:var(--field-color, inherit)}.kt-select__trigger:disabled{cursor:not-allowed;background:var(--field-disabled-bg, #f1f3f4);color:color-mix(in srgb,currentColor 50%,transparent)}.kt-select__value{flex:1;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kt-select__placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__arrow{flex:none;font-family:Material Symbols Outlined;font-size:1.25em;line-height:1;font-feature-settings:\"liga\";color:var(--field-icon-color, #5f6368);transition:var(--select-arrow-transition, transform .12s ease);-webkit-font-smoothing:antialiased}.kt-select--open .kt-select__arrow{transform:rotate(180deg)}.kt-select__popup{box-sizing:border-box;display:flex;flex-direction:column;margin:0;padding:0;border-width:var(--select-popup-border-width, var(--field-border-width, 1px));border-style:var(--field-border-style, solid);border-color:var(--field-border-color, #c4c7c5);border-radius:var(--field-radius, 8px);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);box-shadow:var(--select-popup-shadow, 0 4px 12px rgb(0 0 0 / 12%));-webkit-backdrop-filter:var(--select-popup-backdrop-filter, none);backdrop-filter:var(--select-popup-backdrop-filter, none);max-block-size:var(--select-popup-max-height, 16rem);overflow:hidden}.kt-select__sheet-card{display:contents}.kt-select__popup:not(:popover-open){display:none!important}.kt-select__popup:popover-open{animation:var(--select-popup-enter-animation, none)}@media(prefers-reduced-motion:reduce){.kt-select__popup:popover-open{animation:none}}.kt-select__listbox{flex:1 1 auto;min-block-size:0;margin:0;padding:.25rem;list-style:none;overflow-y:auto}.kt-select__panel{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0}.kt-select__filter{flex:none;padding:.5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__filter-input{box-sizing:border-box;inline-size:100%;min-block-size:var(--field-min-height, 44px);padding:.375rem .625rem;border:var(--field-border-width, 1px) var(--field-border-style, solid) var(--field-border-color, #c4c7c5);border-radius:calc(var(--field-radius, 8px) * .75);background:var(--select-popup-bg, var(--kt-surface, #fff));color:var(--field-color, inherit);font:inherit;appearance:none}.kt-select__filter-input::placeholder{color:var(--field-hint-color, #5f6368)}.kt-select__filter-input:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-1px}.kt-select__filter-input::-webkit-search-cancel-button{appearance:none;inline-size:1rem;block-size:1rem;margin-inline-start:.375rem;cursor:pointer;background-color:var(--field-icon-color, #5f6368);-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat;mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E\") center / contain no-repeat}.kt-select__sr-only{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@supports (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto;top:anchor(bottom);left:anchor(left);margin-block-start:.25rem;min-inline-size:anchor-size(width);width:max-content;max-inline-size:min(90vw,28rem);position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline}}@supports not (anchor-name: --x){.kt-select__popup{position:fixed;inset:auto 0 0;inline-size:100%;max-inline-size:100%;border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0}}.kt-select__popup--sheet{position:fixed!important;inset:0!important;inline-size:100%!important;max-inline-size:100%!important;min-inline-size:0!important;width:100%!important;height:100%!important;max-block-size:none!important;margin:0!important;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important;border-radius:0!important;overflow:visible!important;display:flex;flex-direction:column!important;justify-content:flex-end!important}.kt-select__popup--sheet .kt-select__sheet-card{position:relative;z-index:2;display:flex;flex-direction:column;background:var(--select-popup-bg, var(--kt-surface, #fff));border-radius:var(--kt-sheet-radius, 16px) var(--kt-sheet-radius, 16px) 0 0;box-shadow:var(--kt-sheet-shadow, 0 -4px 16px rgb(0 0 0 / 12%));max-block-size:var(--kt-sheet-max-block-size, 85svh);width:100%;overflow:hidden;translate:0 0;transition:translate var(--kt-sheet-anim-duration, .12s) ease}.kt-select__popup--sheet .kt-select__sheet-card:has(.kt-select__filter){block-size:var(--kt-sheet-max-block-size, 85svh);max-block-size:var(--kt-sheet-max-block-size, 85svh)}.kt-select__popup--sheet:popover-open .kt-select__sheet-card{translate:0 0;animation:var(--select-sheet-enter-animation, kt-sheet-in var(--kt-sheet-anim-duration, .12s) ease)}.kt-select__popup--sheet .kt-select__sheet-card.kt-select__popup--dragging{transition:none}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-card,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-card{translate:0 100%;transition:translate var(--kt-sheet-exit-duration, 90ms) cubic-bezier(.4,0,.2,1)}::ng-deep .kt-select__popup--sheet::backdrop{display:none!important}.kt-select__sheet-scrim{display:none}.kt-select__popup--sheet .kt-select__sheet-scrim{display:block;position:fixed;inset:0;z-index:1;background:var(--kt-sheet-scrim, rgb(0 0 0 / 40%));opacity:0;pointer-events:auto;transition:opacity var(--kt-sheet-anim-duration, .12s) ease,overlay var(--kt-sheet-anim-duration, .12s) allow-discrete,display var(--kt-sheet-anim-duration, .12s) allow-discrete}.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:1}@starting-style{.kt-select__popup--sheet:popover-open .kt-select__sheet-scrim{opacity:0}}.kt-select__popup--sheet.kt-select__popup--closing .kt-select__sheet-scrim,.kt-select__popup--sheet:popover-open.kt-select__popup--closing .kt-select__sheet-scrim{opacity:0;transition:opacity var(--kt-sheet-exit-duration, 90ms) ease}.kt-select__popup--sheet .kt-select__option{--select-option-min-height: 44px;padding-block:.625rem}.kt-select__popup--sheet .kt-select__filter-input{font-size:1rem;min-block-size:44px}@media(prefers-reduced-motion:reduce){.kt-select__popup--sheet .kt-select__sheet-card,.kt-select__popup--sheet .kt-select__sheet-scrim{transition:none;translate:0 0}}.kt-select__sheet-grab{display:flex;align-items:center;justify-content:center;flex:none;block-size:44px;cursor:grab;touch-action:none}.kt-select__sheet-grab:active{cursor:grabbing}.kt-select__sheet-grab:before{content:\"\";inline-size:2.25rem;block-size:.25rem;border-radius:999px;background:var(--kt-sheet-grab-color, var(--kt-outline, #c4c7c5))}.kt-select__sheet-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex:none;padding-block:.5rem;padding-inline:1rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__sheet-title{font-weight:600}.kt-select__sheet-close{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:44px;block-size:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__sheet-close:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:2px}.kt-select__sheet-close-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.5rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__option{display:flex;align-items:center;gap:.5rem;box-sizing:border-box;min-block-size:var( --select-option-min-height, 44px );padding:.5rem .625rem;border-radius:6px;cursor:pointer}.kt-select__option[aria-selected=true]{background:var(--select-option-selected-bg, color-mix(in srgb, var(--kt-primary, #0b57d0) 14%, transparent));color:var(--select-option-selected-color, inherit);font-weight:var(--select-option-selected-weight, 600)}.kt-select__option--active:not([aria-disabled=true]),.kt-select__option:hover:not([aria-disabled=true]){background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__option[aria-disabled=true]{opacity:.5;cursor:not-allowed}.kt-select__empty{padding:.5rem .625rem;color:var(--field-hint-color, #5f6368)}.kt-select__truncated-info{box-sizing:border-box;padding:.5rem .625rem;font-size:.875rem;font-style:italic;color:var(--field-hint-color, #5f6368);border-block-start:1px solid var(--field-border-color, #c4c7c5);pointer-events:none}}\n", "@layer kt-aaa.components{.kt-select__trigger--clearable .kt-select__value{margin-inline-end:2rem}.kt-select__clear{position:absolute;inset-block-start:50%;inset-inline-end:2.5rem;translate:0 -50%;display:inline-flex;align-items:center;justify-content:center;inline-size:24px;block-size:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--field-icon-color, #5f6368);cursor:pointer}.kt-select__clear:after{content:\"\";position:absolute;inset:-10px}.kt-select__clear:hover{background-color:color-mix(in srgb,currentColor 12%,transparent);color:var(--field-color, inherit)}.kt-select__clear:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:1px}.kt-select__clear-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.25rem;line-height:1;-webkit-font-smoothing:antialiased}.kt-select__actions{display:flex;gap:.25rem;flex:none;padding:.25rem .5rem;border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__action{flex:1;min-block-size:44px;padding:.375rem .625rem;border:0;border-radius:calc(var(--field-radius, 8px) * .75);background:transparent;color:var(--kt-primary, #0b57d0);font:inherit;font-weight:500;cursor:pointer}.kt-select__action:hover{background:var(--select-option-hover-bg, color-mix(in srgb, currentColor 8%, transparent))}.kt-select__action:focus-visible{outline:2px solid var(--field-border-color-focus, #0b57d0);outline-offset:-2px}.kt-select__count{flex:none;padding:.25rem .75rem;font-size:.8125rem;color:var(--field-hint-color, #5f6368);border-block-end:1px solid var(--field-border-color, #c4c7c5)}.kt-select__checkbox{display:inline-flex;align-items:center;justify-content:center;flex:none;margin-inline-end:.25rem;-webkit-user-select:none;user-select:none}.kt-select__checkbox-icon{font-family:Material Symbols Outlined;font-feature-settings:\"liga\";font-size:1.25rem;line-height:1;color:var(--field-icon-color, #5f6368);-webkit-font-smoothing:antialiased}.kt-select__option[aria-selected=true] .kt-select__checkbox-icon{color:var(--kt-primary, #0b57d0)}.kt-select__option--multiple{display:flex;align-items:center;gap:.5rem}.kt-select__option-content{flex:1;min-inline-size:0}kt-chip-list:not([data-empty]){margin-block-start:.5rem}}\n"] }]
|
|
2878
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], selectionActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionActions", required: false }] }], maxVisibleChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleChips", required: false }] }], optionDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtMultiSelectOptionDef), { isSignal: true }] }], triggerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtMultiSelectTriggerDef), { isSignal: true }] }], chipDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KtMultiSelectChipDef), { isSignal: true }] }], chipList: [{ type: i0.ViewChild, args: [i0.forwardRef(() => KtChipList), { isSignal: true }] }] } });
|
|
2879
|
+
|
|
2880
|
+
/** Base partagée des champs Temporal (Date/Time/DateTime/YearMonth).
|
|
2881
|
+
Toute la logique commune (parsing tolérant, valeur vide `null`, `displayValue` pour l'input
|
|
2882
|
+
natif) vit ici ; chaque sous-classe ne fournit que la construction et la sérialisation propres
|
|
2883
|
+
à son type Temporal (`fromString` / `serialize`).
|
|
2884
|
+
|
|
2885
|
+
Contrat abstrait : une sous-classe implémente uniquement le couple
|
|
2886
|
+
`fromString` (chaîne ISO de l'input natif → valeur typée `T`) et `serialize`
|
|
2887
|
+
(valeur `T` → chaîne ISO pour l'attribut `[value]` de l'input). La valeur exposée
|
|
2888
|
+
reste toujours `T | null`, où `null` représente le champ vide ; le parsing est
|
|
2889
|
+
tolérant (saisie partielle/invalide → `null`, jamais d'exception ni de valeur fausse).
|
|
2890
|
+
|
|
2891
|
+
@template T Type Temporal porté par le champ (ex. `Temporal.PlainDate`,
|
|
2892
|
+
`Temporal.PlainTime`, …). Doit exposer `toString()`. La valeur du champ est `T | null`. */
|
|
2893
|
+
class KtBaseTemporalField extends KtBaseInputField {
|
|
2894
|
+
doc = inject(DOCUMENT);
|
|
2895
|
+
constructor() {
|
|
2896
|
+
super();
|
|
2897
|
+
effect(() => {
|
|
2898
|
+
const val = this.value();
|
|
2899
|
+
const ref = this.inputRef();
|
|
2900
|
+
if (!ref)
|
|
2901
|
+
return;
|
|
2902
|
+
const inputEl = ref.nativeElement;
|
|
2903
|
+
const serialized = val === null ? '' : this.serialize(val);
|
|
2904
|
+
const isFocused = this.doc.activeElement === inputEl;
|
|
2905
|
+
if (inputEl.value !== serialized && (!isFocused || val !== null)) {
|
|
2906
|
+
inputEl.value = serialized;
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
parse(raw) {
|
|
2911
|
+
if (raw === '')
|
|
2912
|
+
return null;
|
|
2913
|
+
try {
|
|
2914
|
+
return this.fromString(raw);
|
|
2915
|
+
}
|
|
2916
|
+
catch {
|
|
2917
|
+
// Saisie partielle/invalide (RangeError) → vide, jamais une valeur fausse.
|
|
2918
|
+
return null;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
emptyValue() {
|
|
2922
|
+
return null;
|
|
2923
|
+
}
|
|
2924
|
+
isEmpty(value) {
|
|
2925
|
+
return value === null;
|
|
2926
|
+
}
|
|
2927
|
+
/** Représentation ISO pour l'attribut `[value]` de l'input natif (null → champ vide). */
|
|
2928
|
+
displayValue = computed(() => {
|
|
2929
|
+
const value = this.value();
|
|
2930
|
+
return value === null ? '' : this.serialize(value);
|
|
2931
|
+
}, /* @ts-ignore */
|
|
2932
|
+
...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
2933
|
+
/** Suggestions proposées via un `<datalist>` natif (le picker date/heure reste disponible).
|
|
2934
|
+
Valeurs Temporal simples ou couples `{ value, label }` ; chaque valeur est sérialisée au
|
|
2935
|
+
format de l'input via `serialize`. @default undefined */
|
|
2936
|
+
suggestions = input(/* @ts-ignore */
|
|
2937
|
+
...(ngDevMode ? [undefined, { debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
2938
|
+
idGen = inject(KtIdGenerator);
|
|
2939
|
+
uid = this.idGen.generateId('temporal-field');
|
|
2940
|
+
datalistId = `kt-temporal-field-list-${this.uid}`;
|
|
2941
|
+
hasSuggestions = computed(() => (this.suggestions()?.length ?? 0) > 0, /* @ts-ignore */
|
|
2942
|
+
...(ngDevMode ? [{ debugName: "hasSuggestions" }] : /* istanbul ignore next */ []));
|
|
2943
|
+
datalistOptions = computed(() => normalizeKtSuggestions(this.suggestions(), (value) => this.serialize(value)), /* @ts-ignore */
|
|
2944
|
+
...(ngDevMode ? [{ debugName: "datalistOptions" }] : /* istanbul ignore next */ []));
|
|
2945
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseTemporalField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2946
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtBaseTemporalField, isStandalone: true, inputs: { suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0 });
|
|
2947
|
+
}
|
|
2948
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtBaseTemporalField, decorators: [{
|
|
2949
|
+
type: Directive
|
|
2950
|
+
}], ctorParameters: () => [], propDecorators: { suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }] } });
|
|
2951
|
+
|
|
2952
|
+
/** Valeur runtime `Temporal` (native ou polyfill). Cast via `unknown` : selon le contexte de
|
|
2953
|
+
compilation, `globalThis.Temporal` peut déjà être typé (augmentation `temporal-polyfill/global`)
|
|
2954
|
+
ou non — ce cast fonctionne dans les deux cas. */
|
|
2955
|
+
const rawTemporal = globalThis.Temporal;
|
|
2956
|
+
if (typeof rawTemporal === 'undefined') {
|
|
2957
|
+
throw new Error(`[ktortu/aaa] L'espace de noms 'Temporal' est introuvable. ` +
|
|
2958
|
+
`Veuillez vous assurer que le polyfill 'temporal-polyfill/global' est correctement importé ` +
|
|
2959
|
+
`dans le fichier 'main.ts' de votre application.`);
|
|
2960
|
+
}
|
|
2961
|
+
const Temporal = rawTemporal;
|
|
2962
|
+
|
|
2963
|
+
/** Champ « date sans heure » : valeur = `Temporal.PlainDate`, input natif `type="date"`.
|
|
2964
|
+
*
|
|
2965
|
+
* @example
|
|
2966
|
+
* ```html
|
|
2967
|
+
* <kt-date-field label="Date de naissance" [(value)]="birthDate" />
|
|
2968
|
+
* ```
|
|
2969
|
+
*/
|
|
2970
|
+
class KtDateField extends KtBaseTemporalField {
|
|
2971
|
+
/** Valeur du champ : un `Temporal.PlainDate` (date civile sans heure ni fuseau).
|
|
2972
|
+
`null` = champ vide.
|
|
2973
|
+
@default null */
|
|
2974
|
+
value = model(null, /* @ts-ignore */
|
|
2975
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
2976
|
+
fromString(raw) {
|
|
2977
|
+
return Temporal.PlainDate.from(raw);
|
|
2978
|
+
}
|
|
2979
|
+
serialize(value) {
|
|
2980
|
+
return value.toString();
|
|
2981
|
+
}
|
|
2982
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDateField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
2983
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtDateField, isStandalone: true, selector: "kt-date-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"date\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2984
|
+
}
|
|
2985
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDateField, decorators: [{
|
|
2986
|
+
type: Component,
|
|
2987
|
+
args: [{ selector: 'kt-date-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"date\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
2988
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
2989
|
+
|
|
2990
|
+
/** Champ « heure sans date » : valeur = `Temporal.PlainTime`, input natif `type="time"`.
|
|
2991
|
+
*
|
|
2992
|
+
* @example
|
|
2993
|
+
* ```html
|
|
2994
|
+
* <kt-time-field label="Heure de rendez-vous" [(value)]="appointmentTime" />
|
|
2995
|
+
* ```
|
|
2996
|
+
*/
|
|
2997
|
+
class KtTimeField extends KtBaseTemporalField {
|
|
2998
|
+
/** Valeur du champ : un `Temporal.PlainTime` (heure « au mur », sans date ni fuseau).
|
|
2999
|
+
`null` = champ vide.
|
|
3000
|
+
@default null */
|
|
3001
|
+
value = model(null, /* @ts-ignore */
|
|
3002
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3003
|
+
fromString(raw) {
|
|
3004
|
+
return Temporal.PlainTime.from(raw);
|
|
3005
|
+
}
|
|
3006
|
+
serialize(value) {
|
|
3007
|
+
// Précision minute : un input `type="time"` ne saisit pas les secondes par défaut.
|
|
3008
|
+
return value.toString({ smallestUnit: 'minute' });
|
|
3009
|
+
}
|
|
3010
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTimeField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3011
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtTimeField, isStandalone: true, selector: "kt-time-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"time\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3012
|
+
}
|
|
3013
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTimeField, decorators: [{
|
|
3014
|
+
type: Component,
|
|
3015
|
+
args: [{ selector: 'kt-time-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"time\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
3016
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
3017
|
+
|
|
3018
|
+
/** Champ « date-heure sans fuseau » : valeur = `Temporal.PlainDateTime`, input
|
|
3019
|
+
natif `type="datetime-local"`. Volontairement SANS timezone (heure « au mur »).
|
|
3020
|
+
*
|
|
3021
|
+
* @example
|
|
3022
|
+
* ```html
|
|
3023
|
+
* <kt-date-time-field label="Début de l'événement" [(value)]="startsAt" />
|
|
3024
|
+
* ```
|
|
3025
|
+
*/
|
|
3026
|
+
class KtDateTimeField extends KtBaseTemporalField {
|
|
3027
|
+
/** Valeur du champ : un `Temporal.PlainDateTime` (date + heure « au mur », sans fuseau).
|
|
3028
|
+
`null` = champ vide.
|
|
3029
|
+
@default null */
|
|
3030
|
+
value = model(null, /* @ts-ignore */
|
|
3031
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3032
|
+
fromString(raw) {
|
|
3033
|
+
return Temporal.PlainDateTime.from(raw);
|
|
3034
|
+
}
|
|
3035
|
+
serialize(value) {
|
|
3036
|
+
// Précision minute : `type="datetime-local"` ne saisit pas les secondes par défaut.
|
|
3037
|
+
return value.toString({ smallestUnit: 'minute' });
|
|
3038
|
+
}
|
|
3039
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDateTimeField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3040
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtDateTimeField, isStandalone: true, selector: "kt-date-time-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"datetime-local\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3041
|
+
}
|
|
3042
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtDateTimeField, decorators: [{
|
|
3043
|
+
type: Component,
|
|
3044
|
+
args: [{ selector: 'kt-date-time-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"datetime-local\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
3045
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
3046
|
+
|
|
3047
|
+
/** Champ « mois/année » (ex. expiration de carte) : valeur = `Temporal.PlainYearMonth`,
|
|
3048
|
+
input natif `type="month"`.
|
|
3049
|
+
*
|
|
3050
|
+
* @example
|
|
3051
|
+
* ```html
|
|
3052
|
+
* <kt-year-month-field label="Expiration" [(value)]="cardExpiry" />
|
|
3053
|
+
* ```
|
|
3054
|
+
*/
|
|
3055
|
+
class KtYearMonthField extends KtBaseTemporalField {
|
|
3056
|
+
/** Valeur du champ : un `Temporal.PlainYearMonth` (mois + année, sans jour ni fuseau).
|
|
3057
|
+
`null` = champ vide.
|
|
3058
|
+
@default null */
|
|
3059
|
+
value = model(null, /* @ts-ignore */
|
|
3060
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3061
|
+
fromString(raw) {
|
|
3062
|
+
return Temporal.PlainYearMonth.from(raw);
|
|
3063
|
+
}
|
|
3064
|
+
serialize(value) {
|
|
3065
|
+
return value.toString();
|
|
3066
|
+
}
|
|
3067
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtYearMonthField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3068
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtYearMonthField, isStandalone: true, selector: "kt-year-month-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"month\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3069
|
+
}
|
|
3070
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtYearMonthField, decorators: [{
|
|
3071
|
+
type: Component,
|
|
3072
|
+
args: [{ selector: 'kt-year-month-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"month\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
3073
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
3074
|
+
|
|
3075
|
+
/** Source unique de l'heure courante et du **fuseau local** de l'utilisateur.
|
|
3076
|
+
Injectable (et donc figeable en test) : `vi.useFakeTimers()` ne fige PAS `Temporal.Now`.
|
|
3077
|
+
Règle d'équipe : ne jamais appeler `Temporal.Now.*` en direct ailleurs — passer par `KtClock`. */
|
|
3078
|
+
class KtClock {
|
|
3079
|
+
/** Fuseau IANA local de l'utilisateur (auto-détecté), ex. `"Europe/Paris"`.
|
|
3080
|
+
Sert de zone de référence pour `today()` et pour les conversions UTC↔local.
|
|
3081
|
+
@returns L'identifiant de fuseau IANA. */
|
|
3082
|
+
timeZoneId() {
|
|
3083
|
+
return Temporal.Now.timeZoneId();
|
|
3084
|
+
}
|
|
3085
|
+
/** Instant absolu courant, sur la ligne du temps (UTC), indépendant du fuseau.
|
|
3086
|
+
@returns L'instant présent en `Temporal.Instant`. */
|
|
3087
|
+
now() {
|
|
3088
|
+
return Temporal.Now.instant();
|
|
3089
|
+
}
|
|
3090
|
+
/** Date civile du jour telle qu'observée dans le fuseau local (`timeZoneId()`).
|
|
3091
|
+
@returns Le jour courant en `Temporal.PlainDate`. */
|
|
3092
|
+
today() {
|
|
3093
|
+
return Temporal.Now.plainDateISO(this.timeZoneId());
|
|
3094
|
+
}
|
|
3095
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtClock, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3096
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtClock, providedIn: 'root' });
|
|
3097
|
+
}
|
|
3098
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtClock, decorators: [{
|
|
3099
|
+
type: Injectable,
|
|
3100
|
+
args: [{ providedIn: 'root' }]
|
|
3101
|
+
}] });
|
|
3102
|
+
/** Horloge figée pour les tests : fige l'instant ET le fuseau (sinon un test passe en France mais
|
|
3103
|
+
échoue sur un runner CI en UTC). À fournir via `{ provide: KtClock, useValue: new KtFixedClock(...) }`. */
|
|
3104
|
+
class KtFixedClock extends KtClock {
|
|
3105
|
+
fixedInstant;
|
|
3106
|
+
zone;
|
|
3107
|
+
/** Construit une horloge figée déterministe pour les tests.
|
|
3108
|
+
@param fixedInstant Instant absolu (UTC) renvoyé tel quel par `now()` et utilisé comme base de `today()`.
|
|
3109
|
+
@param zone Fuseau IANA fixe (ex. `"Europe/Paris"`) renvoyé par `timeZoneId()`.
|
|
3110
|
+
*
|
|
3111
|
+
* @example
|
|
3112
|
+
* ```ts
|
|
3113
|
+
* TestBed.configureTestingModule({
|
|
3114
|
+
* providers: [
|
|
3115
|
+
* { provide: KtClock, useValue: new KtFixedClock(Temporal.Instant.from('2026-06-18T10:00:00Z'), 'Europe/Paris') },
|
|
3116
|
+
* ],
|
|
3117
|
+
* });
|
|
3118
|
+
* ```
|
|
3119
|
+
*/
|
|
3120
|
+
constructor(fixedInstant, zone) {
|
|
3121
|
+
super();
|
|
3122
|
+
this.fixedInstant = fixedInstant;
|
|
3123
|
+
this.zone = zone;
|
|
3124
|
+
}
|
|
3125
|
+
timeZoneId() {
|
|
3126
|
+
return this.zone;
|
|
3127
|
+
}
|
|
3128
|
+
now() {
|
|
3129
|
+
return this.fixedInstant;
|
|
3130
|
+
}
|
|
3131
|
+
today() {
|
|
3132
|
+
return this.fixedInstant.toZonedDateTimeISO(this.zone).toPlainDate();
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
/** Champ « instant absolu » : valeur = `Temporal.Instant` (UTC), saisi et affiché en **heure
|
|
3137
|
+
locale** via un input `type="datetime-local"`. Conçu pour le flux serveur-UTC → affichage/édition
|
|
3138
|
+
locale → renvoi UTC. Le fuseau local vient du service `KtClock` (injecté → testable).
|
|
3139
|
+
|
|
3140
|
+
Conversion UTC↔heure locale : à l'affichage, l'`Instant` UTC est projeté dans le fuseau
|
|
3141
|
+
local (`KtClock.timeZoneId()`) pour produire l'heure « au mur » montrée à l'utilisateur ; à la
|
|
3142
|
+
saisie, cette heure locale est réinterprétée dans le même fuseau puis reconvertie en `Instant`
|
|
3143
|
+
UTC. Fournir un `KtClock` figé en test garantit une conversion déterministe.
|
|
3144
|
+
*
|
|
3145
|
+
* @example
|
|
3146
|
+
* ```html
|
|
3147
|
+
* <kt-instant-field label="Horodatage" [(value)]="recordedAt" />
|
|
3148
|
+
* ```
|
|
3149
|
+
*/
|
|
3150
|
+
class KtInstantField extends KtBaseTemporalField {
|
|
3151
|
+
clock = inject(KtClock);
|
|
3152
|
+
/** Valeur du champ : un `Temporal.Instant` (instant absolu en UTC), saisi/affiché en heure
|
|
3153
|
+
locale via `KtClock`. `null` = champ vide.
|
|
3154
|
+
@default null */
|
|
3155
|
+
value = model(null, /* @ts-ignore */
|
|
3156
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3157
|
+
// Heure locale saisie (wall-clock) → instant absolu. Disambiguation 'compatible' par défaut
|
|
3158
|
+
// (ne lève pas sur un créneau DST ambigu).
|
|
3159
|
+
fromString(raw) {
|
|
3160
|
+
return Temporal.PlainDateTime.from(raw).toZonedDateTime(this.clock.timeZoneId()).toInstant();
|
|
3161
|
+
}
|
|
3162
|
+
// Instant absolu → heure locale (précision minute, comme un input datetime-local).
|
|
3163
|
+
serialize(value) {
|
|
3164
|
+
return value.toZonedDateTimeISO(this.clock.timeZoneId()).toPlainDateTime().toString({ smallestUnit: 'minute' });
|
|
3165
|
+
}
|
|
3166
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtInstantField, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3167
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: KtInstantField, isStandalone: true, selector: "kt-instant-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"datetime-local\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n", dependencies: [{ kind: "component", type: KtField, selector: "kt-field", inputs: ["label", "hint", "helpText", "helpLabel", "customDescribedBy", "errors", "invalid", "required", "fieldId", "hideHintWhenInvalid", "showAllErrors"], outputs: ["helpClick"] }, { kind: "directive", type: KtFieldControl, selector: "[ktFieldControl]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3168
|
+
}
|
|
3169
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtInstantField, decorators: [{
|
|
3170
|
+
type: Component,
|
|
3171
|
+
args: [{ selector: 'kt-instant-field', imports: [KtField, KtFieldControl, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<kt-field\n [label]=\"label()\"\n [hint]=\"hint()\"\n [helpText]=\"helpText()\"\n [helpLabel]=\"helpLabel()\"\n [customDescribedBy]=\"customDescribedBy()\"\n [errors]=\"errors()\"\n [invalid]=\"showInvalid()\"\n [required]=\"required()\"\n [fieldId]=\"id()\"\n (helpClick)=\"helpClick.emit($event)\"\n>\n <ng-content select=\"[ktFieldHelp]\" />\n <div\n class=\"kt-field-box\"\n [attr.data-invalid]=\"showInvalid() ? '' : null\"\n [attr.data-disabled]=\"disabled() ? '' : null\"\n >\n @if (icon(); as icon) {\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">{{ icon }}</span>\n }\n\n @if (prefix(); as prefix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(prefix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(prefix) }}\n }\n </span>\n }\n\n <input\n #input\n ktFieldControl\n class=\"kt-field-box__input\"\n type=\"datetime-local\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [attr.name]=\"name() || null\"\n [attr.placeholder]=\"placeholder()\"\n [attr.list]=\"hasSuggestions() ? datalistId : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"touched.set(true)\"\n />\n\n @if (hasSuggestions()) {\n <datalist [id]=\"datalistId\">\n @for (option of datalistOptions(); track $index) {\n <option [value]=\"option.value\" [attr.label]=\"option.label\"></option>\n }\n </datalist>\n }\n\n @if (showClear()) {\n <button type=\"button\" class=\"kt-field-box__clear\" [attr.aria-label]=\"clearLabel()\" (click)=\"clear()\">\n <span class=\"kt-field-box__icon\" aria-hidden=\"true\">close</span>\n </button>\n }\n\n @if (suffix(); as suffix) {\n <span class=\"kt-field-box__affix\">\n @if (asTemplate(suffix); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n } @else {\n {{ asText(suffix) }}\n }\n </span>\n }\n </div>\n</kt-field>\n" }]
|
|
3172
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
3173
|
+
|
|
3174
|
+
/** Formate une valeur Temporal pour l'affichage selon la locale active.
|
|
3175
|
+
`DatePipe` n'accepte pas Temporal (seulement `Date|string|number`) → pipe dédié, basé sur
|
|
3176
|
+
`toLocaleString` (interface `Intl.DateTimeFormat`). Cohérent avec la neutralité i18n de la lib
|
|
3177
|
+
(format au runtime via `LOCALE_ID`, pas `$localize`).
|
|
3178
|
+
|
|
3179
|
+
Un `Instant` n'a pas de fuseau propre → il est affiché dans la **zone locale** (via `KtClock`).
|
|
3180
|
+
Un `ZonedDateTime` est affiché dans **sa propre** zone. Les `Plain*` n'ont pas de fuseau.
|
|
3181
|
+
*
|
|
3182
|
+
* @example
|
|
3183
|
+
* ```html
|
|
3184
|
+
* <!-- format par défaut de la locale active -->
|
|
3185
|
+
* {{ d | temporalDate }}
|
|
3186
|
+
* <!-- options Intl.DateTimeFormat -->
|
|
3187
|
+
* {{ d | temporalDate:{ month: 'long' } }}
|
|
3188
|
+
* {{ d | temporalDate:{ dateStyle: 'full' } }}
|
|
3189
|
+
* ```
|
|
3190
|
+
*/
|
|
3191
|
+
class KtTemporalDatePipe {
|
|
3192
|
+
locale = inject(LOCALE_ID);
|
|
3193
|
+
clock = inject(KtClock);
|
|
3194
|
+
transform(value, options) {
|
|
3195
|
+
if (value == null)
|
|
3196
|
+
return '';
|
|
3197
|
+
if (value instanceof Temporal.Instant) {
|
|
3198
|
+
return value.toZonedDateTimeISO(this.clock.timeZoneId()).toLocaleString(this.locale, options);
|
|
3199
|
+
}
|
|
3200
|
+
return value.toLocaleString(this.locale, options);
|
|
3201
|
+
}
|
|
3202
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTemporalDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
3203
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: KtTemporalDatePipe, isStandalone: true, name: "temporalDate" });
|
|
3204
|
+
}
|
|
3205
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTemporalDatePipe, decorators: [{
|
|
3206
|
+
type: Pipe,
|
|
3207
|
+
args: [{ name: 'temporalDate' }]
|
|
3208
|
+
}] });
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* Generated bundle index. Do not edit.
|
|
3212
|
+
*/
|
|
3213
|
+
|
|
3214
|
+
export { KT_CHIPS_CONFIG, KT_FIELD, KT_FIELD_CONFIG, KT_SELECT_CONFIG, KtBaseTemporalField, KtCheckbox, KtCheckboxGroup, KtChip, KtChipItemDef, KtChipList, KtClock, KtDateField, KtDateTimeField, KtField, KtFieldControl, KtFixedClock, KtInstantField, KtMultiSelect, KtMultiSelectChipDef, KtMultiSelectOptionDef, KtMultiSelectTriggerDef, KtNumberField, KtRadio, KtRadioGroup, KtSelect, KtSelectConfig, KtSelectOptionDef, KtSelectTriggerDef, KtSwitch, KtTemporalDatePipe, KtTextArea, KtTextField, KtTimeField, KtYearMonthField, defaultKtFieldErrorMatcher };
|
|
3215
|
+
//# sourceMappingURL=ktortu-aaa-forms.mjs.map
|