ps-helix 6.2.2 → 6.2.3
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 +25 -15
- package/fesm2022/ps-helix.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ps-helix.d.ts +19 -17
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.3"
|
|
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.3
|
|
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
|
@@ -3841,12 +3841,13 @@ const RADIO_STYLES = new InjectionToken('RADIO_STYLES', {
|
|
|
3841
3841
|
*/
|
|
3842
3842
|
let radioIdCounter = 0;
|
|
3843
3843
|
class PshRadioComponent {
|
|
3844
|
-
set checkedInput(v) { this.checked.set(v); }
|
|
3845
|
-
set disabledInput(v) { this.disabled.set(v); }
|
|
3846
3844
|
constructor() {
|
|
3847
3845
|
this.config = inject(RADIO_CONFIG);
|
|
3848
3846
|
this.styles = inject(RADIO_STYLES, { optional: true }) ?? [];
|
|
3849
3847
|
this.uniqueId = `radio-${++radioIdCounter}`;
|
|
3848
|
+
/** Holds the `label` input or the projected content — whichever provides the visible label. */
|
|
3849
|
+
this.labelSlot = viewChild('labelSlot', /* @ts-ignore */
|
|
3850
|
+
...(ngDevMode ? [{ debugName: "labelSlot" }] : /* istanbul ignore next */ []));
|
|
3850
3851
|
// Plain signals + manual @Input/@Output to prevent auto-emission
|
|
3851
3852
|
// when parent sets [checked] or [disabled] via template binding.
|
|
3852
3853
|
// model() would auto-emit checkedChange/disabledChange on .set().
|
|
@@ -3892,7 +3893,10 @@ class PshRadioComponent {
|
|
|
3892
3893
|
const labelText = this.label();
|
|
3893
3894
|
if (labelText)
|
|
3894
3895
|
return labelText;
|
|
3895
|
-
|
|
3896
|
+
// No `aria-label` at all when nothing is declared: the `<label>` wrapping the input
|
|
3897
|
+
// already names it from a projected label. A hard-coded fallback would override that
|
|
3898
|
+
// visible text for screen readers (WCAG 2.5.3 "Label in Name").
|
|
3899
|
+
return undefined;
|
|
3896
3900
|
}, /* @ts-ignore */
|
|
3897
3901
|
...(ngDevMode ? [{ debugName: "computedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
3898
3902
|
this.errorMessageId = computed(() => this.error() ? `${this.uniqueId}-error` : undefined, /* @ts-ignore */
|
|
@@ -3903,12 +3907,18 @@ class PshRadioComponent {
|
|
|
3903
3907
|
// so aria-describedby references whichever message is actually rendered.
|
|
3904
3908
|
this.ariaDescribedBy = computed(() => this.errorMessageId() ?? this.successMessageId(), /* @ts-ignore */
|
|
3905
3909
|
...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3910
|
+
}
|
|
3911
|
+
set checkedInput(v) { this.checked.set(v); }
|
|
3912
|
+
set disabledInput(v) { this.disabled.set(v); }
|
|
3913
|
+
ngAfterViewInit() {
|
|
3914
|
+
if (!isDevMode())
|
|
3915
|
+
return;
|
|
3916
|
+
// Read the rendered label slot rather than the `label` input alone: a projected
|
|
3917
|
+
// label (`<psh-radio>Accept terms</psh-radio>`) is just as accessible, and
|
|
3918
|
+
// `contentChild` cannot see a bare text node. Checked once, after the first render.
|
|
3919
|
+
const hasVisibleLabel = !!this.labelSlot()?.nativeElement.textContent?.trim();
|
|
3920
|
+
if (!hasVisibleLabel && !this.ariaLabel() && !this.hasProjectedContent()) {
|
|
3921
|
+
console.warn('[psh-radio] No accessible label provided. Please use label input, ariaLabel input, or projected content.');
|
|
3912
3922
|
}
|
|
3913
3923
|
}
|
|
3914
3924
|
getState() {
|
|
@@ -3921,9 +3931,9 @@ class PshRadioComponent {
|
|
|
3921
3931
|
return this.checked() ? 'checked' : 'unchecked';
|
|
3922
3932
|
}
|
|
3923
3933
|
/**
|
|
3924
|
-
* Declares whether a label is projected
|
|
3925
|
-
*
|
|
3926
|
-
*
|
|
3934
|
+
* Declares whether a label is projected. No longer required — projected content is now
|
|
3935
|
+
* detected from the rendered label slot. Kept for backwards compatibility: calling it
|
|
3936
|
+
* with `true` still suppresses the dev-only accessibility warning.
|
|
3927
3937
|
*/
|
|
3928
3938
|
updateProjectedContent(hasContent) {
|
|
3929
3939
|
this.hasProjectedContent.set(hasContent);
|
|
@@ -3969,7 +3979,7 @@ class PshRadioComponent {
|
|
|
3969
3979
|
// Implementation requires radio group context
|
|
3970
3980
|
}
|
|
3971
3981
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PshRadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3972
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: PshRadioComponent, isStandalone: true, selector: "psh-radio", inputs: { checkedInput: { classPropertyName: "checkedInput", publicName: "checked", isSignal: false, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, success: { classPropertyName: "success", publicName: "success", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checkedChange: "checkedChange", disabledChange: "disabledChange", valueChange: "valueChange" }, host: { properties: { "class.radio-disabled": "disabled()", "class.radio-error": "!!error()", "class.radio-success": "!!success()", "class.radio-small": "size() === \"small\"", "class.radio-large": "size() === \"large\"", "attr.data-state": "state()" } }, ngImport: i0, template: "<div class=\"radio-container\">\r\n <label class=\"radio-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n type=\"radio\"\r\n class=\"radio-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [name]=\"name()\"\r\n [attr.value]=\"value()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-checked]=\"checked()\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"handleChange()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n <span \r\n class=\"radio-control\"\r\n [class.error]=\"!!error()\"\r\n [class.success]=\"!!success()\"\r\n >\r\n <span class=\"radio-dot\"></span>\r\n </span>\r\n <span class=\"radio-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content
|
|
3982
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: PshRadioComponent, isStandalone: true, selector: "psh-radio", inputs: { checkedInput: { classPropertyName: "checkedInput", publicName: "checked", isSignal: false, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, success: { classPropertyName: "success", publicName: "success", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checkedChange: "checkedChange", disabledChange: "disabledChange", valueChange: "valueChange" }, host: { properties: { "class.radio-disabled": "disabled()", "class.radio-error": "!!error()", "class.radio-success": "!!success()", "class.radio-small": "size() === \"small\"", "class.radio-large": "size() === \"large\"", "attr.data-state": "state()" } }, viewQueries: [{ propertyName: "labelSlot", first: true, predicate: ["labelSlot"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"radio-container\">\r\n <label class=\"radio-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n type=\"radio\"\r\n class=\"radio-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [name]=\"name()\"\r\n [attr.value]=\"value()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-checked]=\"checked()\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"handleChange()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n <span \r\n class=\"radio-control\"\r\n [class.error]=\"!!error()\"\r\n [class.success]=\"!!success()\"\r\n >\r\n <span class=\"radio-dot\"></span>\r\n </span>\r\n <span class=\"radio-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]=\"errorMessageId()\" class=\"radio-error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"successMessageId()\" class=\"radio-success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [".radio-container{display:inline-flex;flex-direction:column;gap:var(--form-field-gap)}.radio-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)}.radio-label.label-left{flex-direction:row-reverse;justify-content:space-between}.radio-input{position:absolute;opacity:0;width:0;height:0}.radio-control{display:flex;align-items:center;justify-content:center;width:var(--size-5);height:var(--size-5);min-width:var(--size-5);min-height:var(--size-5);border:var(--border-width-2) solid var(--surface-border);border-radius:var(--radius-full);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);flex-shrink:0}.radio-dot{width:var(--size-2);height:var(--size-2);border-radius:var(--radius-full);background:var(--primary-color-text);opacity:0;transform:scale(.8);transition:all var(--animation-duration-fast) var(--animation-easing-default)}.radio-input:checked+.radio-control{background:var(--primary-color);border-color:var(--primary-color)}.radio-input:checked+.radio-control .radio-dot{opacity:1;transform:scale(1)}.radio-input:focus-visible+.radio-control{box-shadow:0 0 0 var(--focus-outline-width) var(--focus-ring-color)}:host(.radio-disabled) .radio-label{opacity:var(--opacity-disabled);cursor:not-allowed}:host(.radio-error) .radio-control{border-color:var(--danger-color)}:host(.radio-error) .radio-input:checked+.radio-control{background:var(--danger-color);border-color:var(--danger-color)}:host(.radio-success) .radio-control{border-color:var(--success-color)}:host(.radio-success) .radio-input:checked+.radio-control{background:var(--success-color);border-color:var(--success-color)}.radio-error{font-size:var(--font-size-sm);color:var(--danger-color)}.radio-success{font-size:var(--font-size-sm);color:var(--success-color)}:host(.radio-small) .radio-control{width:var(--size-4);height:var(--size-4);min-width:var(--size-4);min-height:var(--size-4)}:host(.radio-small) .radio-dot{width:var(--size-1-5);height:var(--size-1-5)}:host(.radio-small) .radio-text{font-size:var(--font-size-sm)}:host(.radio-large) .radio-control{width:var(--size-6);height:var(--size-6);min-width:var(--size-6);min-height:var(--size-6)}:host(.radio-large) .radio-dot{width:var(--size-3);height:var(--size-3)}:host(.radio-large) .radio-text{font-size:var(--font-size-lg)}.radio-text{color:var(--text-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3973
3983
|
}
|
|
3974
3984
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PshRadioComponent, decorators: [{
|
|
3975
3985
|
type: Component,
|
|
@@ -3980,8 +3990,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
3980
3990
|
'[class.radio-small]': 'size() === "small"',
|
|
3981
3991
|
'[class.radio-large]': 'size() === "large"',
|
|
3982
3992
|
'[attr.data-state]': 'state()'
|
|
3983
|
-
}, template: "<div class=\"radio-container\">\r\n <label class=\"radio-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n type=\"radio\"\r\n class=\"radio-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [name]=\"name()\"\r\n [attr.value]=\"value()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-checked]=\"checked()\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"handleChange()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n <span \r\n class=\"radio-control\"\r\n [class.error]=\"!!error()\"\r\n [class.success]=\"!!success()\"\r\n >\r\n <span class=\"radio-dot\"></span>\r\n </span>\r\n <span class=\"radio-text\">\r\n @if (label()) {\r\n {{ label() }}\r\n } @else {\r\n <ng-content
|
|
3984
|
-
}],
|
|
3993
|
+
}, template: "<div class=\"radio-container\">\r\n <label class=\"radio-label\" [class.label-left]=\"labelPosition() === 'left'\">\r\n <input\r\n type=\"radio\"\r\n class=\"radio-input\"\r\n [checked]=\"checked()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required()\"\r\n [name]=\"name()\"\r\n [attr.value]=\"value()\"\r\n [attr.aria-label]=\"computedAriaLabel()\"\r\n [attr.aria-required]=\"required() || null\"\r\n [attr.aria-invalid]=\"error() ? 'true' : null\"\r\n [attr.aria-checked]=\"checked()\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\"\r\n (change)=\"handleChange()\"\r\n (keydown)=\"handleKeydown($event)\"\r\n />\r\n <span \r\n class=\"radio-control\"\r\n [class.error]=\"!!error()\"\r\n [class.success]=\"!!success()\"\r\n >\r\n <span class=\"radio-dot\"></span>\r\n </span>\r\n <span class=\"radio-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]=\"errorMessageId()\" class=\"radio-error\" role=\"alert\">\r\n {{ error() }}\r\n </div>\r\n } @else if (success()) {\r\n <div [id]=\"successMessageId()\" class=\"radio-success\" role=\"status\">\r\n {{ success() }}\r\n </div>\r\n }\r\n</div>", styles: [".radio-container{display:inline-flex;flex-direction:column;gap:var(--form-field-gap)}.radio-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)}.radio-label.label-left{flex-direction:row-reverse;justify-content:space-between}.radio-input{position:absolute;opacity:0;width:0;height:0}.radio-control{display:flex;align-items:center;justify-content:center;width:var(--size-5);height:var(--size-5);min-width:var(--size-5);min-height:var(--size-5);border:var(--border-width-2) solid var(--surface-border);border-radius:var(--radius-full);background:var(--surface-card);transition:all var(--animation-duration-fast) var(--animation-easing-default);flex-shrink:0}.radio-dot{width:var(--size-2);height:var(--size-2);border-radius:var(--radius-full);background:var(--primary-color-text);opacity:0;transform:scale(.8);transition:all var(--animation-duration-fast) var(--animation-easing-default)}.radio-input:checked+.radio-control{background:var(--primary-color);border-color:var(--primary-color)}.radio-input:checked+.radio-control .radio-dot{opacity:1;transform:scale(1)}.radio-input:focus-visible+.radio-control{box-shadow:0 0 0 var(--focus-outline-width) var(--focus-ring-color)}:host(.radio-disabled) .radio-label{opacity:var(--opacity-disabled);cursor:not-allowed}:host(.radio-error) .radio-control{border-color:var(--danger-color)}:host(.radio-error) .radio-input:checked+.radio-control{background:var(--danger-color);border-color:var(--danger-color)}:host(.radio-success) .radio-control{border-color:var(--success-color)}:host(.radio-success) .radio-input:checked+.radio-control{background:var(--success-color);border-color:var(--success-color)}.radio-error{font-size:var(--font-size-sm);color:var(--danger-color)}.radio-success{font-size:var(--font-size-sm);color:var(--success-color)}:host(.radio-small) .radio-control{width:var(--size-4);height:var(--size-4);min-width:var(--size-4);min-height:var(--size-4)}:host(.radio-small) .radio-dot{width:var(--size-1-5);height:var(--size-1-5)}:host(.radio-small) .radio-text{font-size:var(--font-size-sm)}:host(.radio-large) .radio-control{width:var(--size-6);height:var(--size-6);min-width:var(--size-6);min-height:var(--size-6)}:host(.radio-large) .radio-dot{width:var(--size-3);height:var(--size-3)}:host(.radio-large) .radio-text{font-size:var(--font-size-lg)}.radio-text{color:var(--text-color)}\n"] }]
|
|
3994
|
+
}], propDecorators: { labelSlot: [{ type: i0.ViewChild, args: ['labelSlot', { isSignal: true }] }], checkedInput: [{
|
|
3985
3995
|
type: Input,
|
|
3986
3996
|
args: ['checked']
|
|
3987
3997
|
}], disabledInput: [{
|