ps-helix 6.2.0 → 6.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/fesm2022/ps-helix.mjs +89 -44
- package/fesm2022/ps-helix.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ps-helix.d.ts +28 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive Angular component library built with Angular 22+ featuring modern design patterns, accessibility-first development, and optimal developer experience.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/ps-helix)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://angular.dev/)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
@@ -106,7 +106,7 @@ After installation, verify that ps-helix is in your `package.json`:
|
|
|
106
106
|
```json
|
|
107
107
|
{
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"ps-helix": "^6.2.
|
|
109
|
+
"ps-helix": "^6.2.2"
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
```
|
|
@@ -1182,7 +1182,7 @@ Copyright (c) 2025 PACK Solutions
|
|
|
1182
1182
|
|
|
1183
1183
|
---
|
|
1184
1184
|
|
|
1185
|
-
**Version**: 6.2.
|
|
1185
|
+
**Version**: 6.2.2
|
|
1186
1186
|
**Built with**: Angular 22.0.0, TypeScript 6.0.0, Phosphor Icons 2.0.3
|
|
1187
1187
|
**Author**: Fabrice PEREZ | Design Engineer at PACK Solutions
|
|
1188
1188
|
**Last Updated**: January 2026
|
package/fesm2022/ps-helix.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, computed, ChangeDetectionStrategy, Component, model, inject, ElementRef, signal, PLATFORM_ID, ViewEncapsulation, InjectionToken, viewChild,
|
|
2
|
+
import { input, output, computed, ChangeDetectionStrategy, Component, model, inject, ElementRef, signal, PLATFORM_ID, ViewEncapsulation, InjectionToken, viewChild, isDevMode, Injectable, DestroyRef, Directive, ViewContainerRef, ChangeDetectorRef, effect, ViewChild, Injector, afterNextRender, HostListener, Renderer2, contentChild, linkedSignal, EventEmitter, Output, Input, contentChildren } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
|
|
5
5
|
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
@@ -1210,6 +1210,9 @@ class PshCheckboxComponent {
|
|
|
1210
1210
|
this.config = inject(CHECKBOX_CONFIG);
|
|
1211
1211
|
this.checkboxInput = viewChild('checkboxInput', /* @ts-ignore */
|
|
1212
1212
|
...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
|
|
1213
|
+
/** Holds the `label` input or the projected content — whichever provides the visible label. */
|
|
1214
|
+
this.labelSlot = viewChild('labelSlot', /* @ts-ignore */
|
|
1215
|
+
...(ngDevMode ? [{ debugName: "labelSlot" }] : /* istanbul ignore next */ []));
|
|
1213
1216
|
this.uniqueId = `psh-cb-${++checkboxIdCounter}`;
|
|
1214
1217
|
this.onChange = (_) => { };
|
|
1215
1218
|
this.onTouched = () => { };
|
|
@@ -1261,11 +1264,17 @@ class PshCheckboxComponent {
|
|
|
1261
1264
|
return this.checked() ? 'checked' : 'unchecked';
|
|
1262
1265
|
}, /* @ts-ignore */
|
|
1263
1266
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1267
|
+
}
|
|
1268
|
+
ngAfterViewInit() {
|
|
1269
|
+
if (!isDevMode())
|
|
1270
|
+
return;
|
|
1271
|
+
// Read the rendered label slot rather than the `label` input alone: a projected
|
|
1272
|
+
// label (`<psh-checkbox>Accept terms</psh-checkbox>`) is just as accessible, and
|
|
1273
|
+
// `contentChild` cannot see a bare text node. Checked once, after the first render.
|
|
1274
|
+
const hasVisibleLabel = !!this.labelSlot()?.nativeElement.textContent?.trim();
|
|
1275
|
+
if (!hasVisibleLabel && !this.ariaLabel()) {
|
|
1276
|
+
console.warn('[psh-checkbox] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
|
|
1277
|
+
}
|
|
1269
1278
|
}
|
|
1270
1279
|
toggle() {
|
|
1271
1280
|
if (!this.disabled()) {
|
|
@@ -1296,7 +1305,7 @@ class PshCheckboxComponent {
|
|
|
1296
1305
|
provide: NG_VALUE_ACCESSOR,
|
|
1297
1306
|
useExisting: PshCheckboxComponent,
|
|
1298
1307
|
multi: true
|
|
1299
|
-
}], viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1308
|
+
}], viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }, { propertyName: "labelSlot", first: true, predicate: ["labelSlot"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\" #labelSlot>\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1300
1309
|
}
|
|
1301
1310
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PshCheckboxComponent, decorators: [{
|
|
1302
1311
|
type: Component,
|
|
@@ -1314,8 +1323,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
1314
1323
|
'[class.checkbox-indeterminate]': 'indeterminate()',
|
|
1315
1324
|
'[attr.data-state]': 'state()',
|
|
1316
1325
|
'[attr.aria-checked]': 'ariaChecked()'
|
|
1317
|
-
}, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"] }]
|
|
1318
|
-
}],
|
|
1326
|
+
}, template: "<div class=\"checkbox-container\">\r\n <label class=\"checkbox-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n #checkboxInput\r\n type=\"checkbox\"\r\n class=\"checkbox-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-checked]=\"ariaChecked()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"toggle()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n \r\n <span class=\"checkbox-control\">\r\n @if (checked() && !indeterminate()) {\r\n <i class=\"ph ph-check\" aria-hidden=\"true\"></i>\r\n }\r\n @if (indeterminate()) {\r\n <i class=\"ph ph-minus\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n <span class=\"checkbox-text\" #labelSlot>\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content></ng-content>\r\n }\r\n </span>\r\n </label>\r\n\r\n @if (error()) {\r\n <div [id]=\"uniqueId + '-error'\" class=\"checkbox-message error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"uniqueId + '-success'\" class=\"checkbox-message success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [":host{display:inline-flex;flex-direction:column}.checkbox-container{display:flex;flex-direction:column;gap:var(--form-field-gap)}.checkbox-label{display:inline-flex;align-items:center;gap:var(--form-label-gap);cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--text-color)}.checkbox-label.label-left{flex-direction:row-reverse;justify-content:space-between}.checkbox-input{position:absolute;opacity:0;width:0;height:0}.checkbox-control{display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:.125rem solid var(--surface-border);border-radius:var(--border-radius);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);color:#fff;position:relative;flex-shrink:0}:host.checkbox-checked .checkbox-control,:host.checkbox-indeterminate .checkbox-control{background:var(--primary-color);border-color:var(--primary-color)}.checkbox-input:focus-visible+.checkbox-control{outline:.125rem solid var(--focus-outline-color);outline-offset:.125rem}.checkbox-control i{font-size:.875rem;line-height:1}:host.checkbox-small .checkbox-control{width:1rem;height:1rem}:host.checkbox-large .checkbox-control{width:1.5rem;height:1.5rem}:host.checkbox-error .checkbox-control{border-color:var(--danger-color)}:host.checkbox-error.checkbox-checked .checkbox-control{background:var(--danger-color)}:host.checkbox-success .checkbox-control{border-color:var(--success-color)}:host.checkbox-success.checkbox-checked .checkbox-control{background:var(--success-color)}:host.checkbox-disabled{opacity:var(--opacity-disabled);cursor:not-allowed}:host.checkbox-disabled .checkbox-label{cursor:not-allowed}.checkbox-message{font-size:.875rem}.checkbox-message.error{color:var(--danger-color)}.checkbox-message.success{color:var(--success-color)}\n"] }]
|
|
1327
|
+
}], propDecorators: { checkboxInput: [{ type: i0.ViewChild, args: ['checkboxInput', { isSignal: true }] }], labelSlot: [{ type: i0.ViewChild, args: ['labelSlot', { isSignal: true }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], success: [{ type: i0.Input, args: [{ isSignal: true, alias: "success", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }] } });
|
|
1319
1328
|
|
|
1320
1329
|
class PshCollapseComponent {
|
|
1321
1330
|
constructor() {
|
|
@@ -1325,14 +1334,18 @@ class PshCollapseComponent {
|
|
|
1325
1334
|
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1326
1335
|
this.variant = input('default', { ...(ngDevMode ? { debugName: "variant" } : /* istanbul ignore next */ {}), transform: (value) => {
|
|
1327
1336
|
if (!['default', 'outline'].includes(value)) {
|
|
1328
|
-
|
|
1337
|
+
if (isDevMode()) {
|
|
1338
|
+
console.warn(`[psh-collapse] Invalid variant "${value}", falling back to "default"`);
|
|
1339
|
+
}
|
|
1329
1340
|
return 'default';
|
|
1330
1341
|
}
|
|
1331
1342
|
return value;
|
|
1332
1343
|
} });
|
|
1333
1344
|
this.size = input('medium', { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: (value) => {
|
|
1334
1345
|
if (!['small', 'medium', 'large'].includes(value)) {
|
|
1335
|
-
|
|
1346
|
+
if (isDevMode()) {
|
|
1347
|
+
console.warn(`[psh-collapse] Invalid size "${value}", falling back to "medium"`);
|
|
1348
|
+
}
|
|
1336
1349
|
return 'medium';
|
|
1337
1350
|
}
|
|
1338
1351
|
return value;
|
|
@@ -3505,16 +3518,31 @@ class PshPaginationComponent {
|
|
|
3505
3518
|
this.itemsPerPageInput = input(10, { ...(ngDevMode ? { debugName: "itemsPerPageInput" } : /* istanbul ignore next */ {}), alias: 'itemsPerPage' });
|
|
3506
3519
|
this.itemsPerPage = linkedSignal(this.itemsPerPageInput, /* @ts-ignore */
|
|
3507
3520
|
...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
|
|
3521
|
+
/**
|
|
3522
|
+
* Number of pages used for rendering and navigation: never below 1, and never
|
|
3523
|
+
* written back to the `totalPages` input. `totalPages` is derived data owned by
|
|
3524
|
+
* the caller (`Math.ceil(total / pageSize)`, or a server-side page count), so the
|
|
3525
|
+
* component reads it and clamps its own view of it instead of correcting it.
|
|
3526
|
+
*/
|
|
3527
|
+
this.effectiveTotalPages = computed(() => {
|
|
3528
|
+
const total = this.totalPages();
|
|
3529
|
+
return Number.isInteger(total) && total >= 1 ? total : 1;
|
|
3530
|
+
}, /* @ts-ignore */
|
|
3531
|
+
...(ngDevMode ? [{ debugName: "effectiveTotalPages" }] : /* istanbul ignore next */ []));
|
|
3508
3532
|
this.size = input(this.config.size ?? 'medium', { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: (value) => {
|
|
3509
3533
|
if (!['small', 'medium', 'large'].includes(value)) {
|
|
3510
|
-
|
|
3534
|
+
if (isDevMode()) {
|
|
3535
|
+
console.warn(`[psh-pagination] Invalid size "${value}", falling back to "medium"`);
|
|
3536
|
+
}
|
|
3511
3537
|
return 'medium';
|
|
3512
3538
|
}
|
|
3513
3539
|
return value;
|
|
3514
3540
|
} });
|
|
3515
3541
|
this.variant = input(this.config.variant ?? 'default', { ...(ngDevMode ? { debugName: "variant" } : /* istanbul ignore next */ {}), transform: (value) => {
|
|
3516
3542
|
if (!['default', 'outline'].includes(value)) {
|
|
3517
|
-
|
|
3543
|
+
if (isDevMode()) {
|
|
3544
|
+
console.warn(`[psh-pagination] Invalid variant "${value}", falling back to "default"`);
|
|
3545
|
+
}
|
|
3518
3546
|
return 'default';
|
|
3519
3547
|
}
|
|
3520
3548
|
return value;
|
|
@@ -3559,7 +3587,7 @@ class PshPaginationComponent {
|
|
|
3559
3587
|
const pages = [];
|
|
3560
3588
|
const halfVisible = Math.floor(this.maxVisiblePages() / 2);
|
|
3561
3589
|
let start = Math.max(1, this.currentPage() - halfVisible);
|
|
3562
|
-
const end = Math.min(this.
|
|
3590
|
+
const end = Math.min(this.effectiveTotalPages(), start + this.maxVisiblePages() - 1);
|
|
3563
3591
|
if (end - start + 1 < this.maxVisiblePages()) {
|
|
3564
3592
|
start = Math.max(1, end - this.maxVisiblePages() + 1);
|
|
3565
3593
|
}
|
|
@@ -3571,60 +3599,70 @@ class PshPaginationComponent {
|
|
|
3571
3599
|
...(ngDevMode ? [{ debugName: "pages" }] : /* istanbul ignore next */ []));
|
|
3572
3600
|
this.state = computed(() => this.getState(), /* @ts-ignore */
|
|
3573
3601
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
3574
|
-
this.canGoNext = computed(() => this.currentPage() < this.
|
|
3602
|
+
this.canGoNext = computed(() => this.currentPage() < this.effectiveTotalPages(), /* @ts-ignore */
|
|
3575
3603
|
...(ngDevMode ? [{ debugName: "canGoNext" }] : /* istanbul ignore next */ []));
|
|
3576
3604
|
this.canGoPrevious = computed(() => this.currentPage() > 1, /* @ts-ignore */
|
|
3577
3605
|
...(ngDevMode ? [{ debugName: "canGoPrevious" }] : /* istanbul ignore next */ []));
|
|
3578
3606
|
this.isFirstPage = computed(() => this.currentPage() === 1, /* @ts-ignore */
|
|
3579
3607
|
...(ngDevMode ? [{ debugName: "isFirstPage" }] : /* istanbul ignore next */ []));
|
|
3580
|
-
this.isLastPage = computed(() => this.currentPage() === this.
|
|
3608
|
+
this.isLastPage = computed(() => this.currentPage() === this.effectiveTotalPages(), /* @ts-ignore */
|
|
3581
3609
|
...(ngDevMode ? [{ debugName: "isLastPage" }] : /* istanbul ignore next */ []));
|
|
3582
3610
|
this.currentPageAnnouncement = computed(() => {
|
|
3583
|
-
return `Page ${this.currentPage()} sur ${this.
|
|
3611
|
+
return `Page ${this.currentPage()} sur ${this.effectiveTotalPages()}`;
|
|
3584
3612
|
}, /* @ts-ignore */
|
|
3585
3613
|
...(ngDevMode ? [{ debugName: "currentPageAnnouncement" }] : /* istanbul ignore next */ []));
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
this.totalPages
|
|
3591
|
-
|
|
3592
|
-
|
|
3614
|
+
if (isDevMode()) {
|
|
3615
|
+
effect(() => {
|
|
3616
|
+
// `0` is a legitimate state (empty list, or a server-side page count that is
|
|
3617
|
+
// not loaded yet): stay silent. Only report what betrays a caller bug.
|
|
3618
|
+
const total = this.totalPages();
|
|
3619
|
+
if (total !== 0 && (!Number.isInteger(total) || total < 1)) {
|
|
3620
|
+
console.warn(`[psh-pagination] Invalid totalPages "${total}", using 1 page`);
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3623
|
+
effect(() => {
|
|
3624
|
+
const maxVisible = this.maxVisiblePages();
|
|
3625
|
+
if (maxVisible < 1 || !Number.isFinite(maxVisible)) {
|
|
3626
|
+
console.warn(`[psh-pagination] Invalid maxVisiblePages "${maxVisible}", must be >= 1`);
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
// `currentPage` IS owned by the component (it navigates), so correcting it with
|
|
3631
|
+
// a write-back is the intended behaviour — clamped on the derived page count.
|
|
3593
3632
|
effect(() => {
|
|
3594
3633
|
const current = this.currentPage();
|
|
3595
|
-
const total = this.
|
|
3634
|
+
const total = this.effectiveTotalPages();
|
|
3596
3635
|
if (current < 1 || !Number.isFinite(current)) {
|
|
3597
|
-
|
|
3636
|
+
if (isDevMode()) {
|
|
3637
|
+
console.warn(`[psh-pagination] Invalid currentPage "${current}", setting to 1`);
|
|
3638
|
+
}
|
|
3598
3639
|
this.currentPage.set(1);
|
|
3599
3640
|
}
|
|
3600
|
-
else if (current > total
|
|
3601
|
-
|
|
3641
|
+
else if (current > total) {
|
|
3642
|
+
if (isDevMode()) {
|
|
3643
|
+
console.warn(`[psh-pagination] currentPage "${current}" exceeds totalPages "${total}", setting to ${total}`);
|
|
3644
|
+
}
|
|
3602
3645
|
this.currentPage.set(total);
|
|
3603
3646
|
}
|
|
3604
3647
|
});
|
|
3605
|
-
effect(() => {
|
|
3606
|
-
const maxVisible = this.maxVisiblePages();
|
|
3607
|
-
if (maxVisible < 1 || !Number.isFinite(maxVisible)) {
|
|
3608
|
-
console.warn(`[psh-pagination] Invalid maxVisiblePages "${maxVisible}", must be >= 1`);
|
|
3609
|
-
}
|
|
3610
|
-
});
|
|
3611
3648
|
}
|
|
3612
3649
|
getState() {
|
|
3613
3650
|
if (this.currentPage() === 1)
|
|
3614
3651
|
return 'first';
|
|
3615
|
-
if (this.currentPage() === this.
|
|
3652
|
+
if (this.currentPage() === this.effectiveTotalPages())
|
|
3616
3653
|
return 'last';
|
|
3617
3654
|
return 'default';
|
|
3618
3655
|
}
|
|
3619
3656
|
goToPage(page) {
|
|
3620
|
-
|
|
3657
|
+
const total = this.effectiveTotalPages();
|
|
3658
|
+
if (page !== this.currentPage() && page >= 1 && page <= total) {
|
|
3621
3659
|
this.currentPage.set(page);
|
|
3622
3660
|
this.pageChange.emit(page);
|
|
3623
3661
|
}
|
|
3624
|
-
else if (page < 1 || page >
|
|
3662
|
+
else if (page < 1 || page > total) {
|
|
3625
3663
|
this.navigationError.emit({
|
|
3626
3664
|
action: 'goToPage',
|
|
3627
|
-
reason: `Page ${page} is out of bounds (1-${
|
|
3665
|
+
reason: `Page ${page} is out of bounds (1-${total})`,
|
|
3628
3666
|
});
|
|
3629
3667
|
}
|
|
3630
3668
|
}
|
|
@@ -3632,7 +3670,7 @@ class PshPaginationComponent {
|
|
|
3632
3670
|
this.goToPage(1);
|
|
3633
3671
|
}
|
|
3634
3672
|
goToLastPage() {
|
|
3635
|
-
this.goToPage(this.
|
|
3673
|
+
this.goToPage(this.effectiveTotalPages());
|
|
3636
3674
|
}
|
|
3637
3675
|
goToNextPage() {
|
|
3638
3676
|
if (this.canGoNext()) {
|
|
@@ -3865,11 +3903,13 @@ class PshRadioComponent {
|
|
|
3865
3903
|
// so aria-describedby references whichever message is actually rendered.
|
|
3866
3904
|
this.ariaDescribedBy = computed(() => this.errorMessageId() ?? this.successMessageId(), /* @ts-ignore */
|
|
3867
3905
|
...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3906
|
+
if (isDevMode()) {
|
|
3907
|
+
effect(() => {
|
|
3908
|
+
if (!this.label() && !this.ariaLabel() && !this.hasProjectedContent()) {
|
|
3909
|
+
console.warn('[psh-radio] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
|
|
3910
|
+
}
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3873
3913
|
}
|
|
3874
3914
|
getState() {
|
|
3875
3915
|
if (this.disabled())
|
|
@@ -3880,6 +3920,11 @@ class PshRadioComponent {
|
|
|
3880
3920
|
return 'success';
|
|
3881
3921
|
return this.checked() ? 'checked' : 'unchecked';
|
|
3882
3922
|
}
|
|
3923
|
+
/**
|
|
3924
|
+
* Declares whether a label is projected (`<psh-radio>Label</psh-radio>`). Not detected
|
|
3925
|
+
* automatically yet: until a consumer calls this, a projected label still triggers the
|
|
3926
|
+
* dev-only accessibility warning and `aria-label="Radio"` overrides the visible text.
|
|
3927
|
+
*/
|
|
3883
3928
|
updateProjectedContent(hasContent) {
|
|
3884
3929
|
this.hasProjectedContent.set(hasContent);
|
|
3885
3930
|
}
|