ngx-opalbytes-components 1.25.1 → 1.27.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.
Files changed (24) hide show
  1. package/README.md +497 -0
  2. package/fesm2022/ngx-opalbytes-components.mjs +890 -2
  3. package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/src/lib/shared/components/checkbox/checkbox.component.css +131 -0
  6. package/src/lib/shared/components/checkbox/checkbox.component.html +84 -0
  7. package/src/lib/shared/components/checkbox/checkbox.component.spec.ts +344 -0
  8. package/src/lib/shared/components/checkbox/checkbox.component.ts +258 -0
  9. package/src/lib/shared/components/checkbox/checkbox.stories.ts +240 -0
  10. package/src/lib/shared/components/index.ts +4 -0
  11. package/src/lib/shared/components/radio/radio-button.component.css +95 -0
  12. package/src/lib/shared/components/radio/radio-button.component.html +32 -0
  13. package/src/lib/shared/components/radio/radio-button.component.ts +133 -0
  14. package/src/lib/shared/components/radio/radio-button.stories.ts +94 -0
  15. package/src/lib/shared/components/radio/radio-group.component.css +21 -0
  16. package/src/lib/shared/components/radio/radio-group.component.ts +216 -0
  17. package/src/lib/shared/components/radio/radio.component.spec.ts +128 -0
  18. package/src/lib/shared/components/radio/radio.stories.ts +128 -0
  19. package/src/lib/shared/components/slide-toggle/slide-toggle.component.css +135 -0
  20. package/src/lib/shared/components/slide-toggle/slide-toggle.component.html +48 -0
  21. package/src/lib/shared/components/slide-toggle/slide-toggle.component.spec.ts +155 -0
  22. package/src/lib/shared/components/slide-toggle/slide-toggle.component.ts +233 -0
  23. package/src/lib/shared/components/slide-toggle/slide-toggle.stories.ts +165 -0
  24. package/types/ngx-opalbytes-components.d.ts +228 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-opalbytes-components",
3
- "version": "1.25.1",
3
+ "version": "1.27.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.0",
6
6
  "@angular/core": "^21.0.0",
@@ -0,0 +1,131 @@
1
+ :host {
2
+ --cao-checkbox-color: #4f46e5;
3
+ --cao-checkbox-unselected: #6b7280;
4
+ --cao-checkbox-disabled: #d1d5db;
5
+ --cao-checkbox-label: #111827;
6
+ --cao-checkbox-label-disabled: #9ca3af;
7
+ --cao-checkbox-focus-ring: rgba(79, 70, 229, 0.35);
8
+ --cao-checkbox-size: 20px;
9
+ --cao-checkbox-radius: 4px;
10
+ --cao-checkbox-duration: 200ms;
11
+ --cao-checkbox-icon-size: 14px;
12
+ --cao-checkbox-icon-color: #ffffff;
13
+
14
+ display: inline-flex;
15
+ }
16
+
17
+ .cao-checkbox {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 8px;
21
+ cursor: pointer;
22
+ user-select: none;
23
+ }
24
+
25
+ .cao-checkbox--disabled {
26
+ cursor: not-allowed;
27
+ }
28
+
29
+ .cao-checkbox__input {
30
+ position: absolute;
31
+ width: 1px;
32
+ height: 1px;
33
+ margin: -1px;
34
+ padding: 0;
35
+ border: 0;
36
+ overflow: hidden;
37
+ clip: rect(0 0 0 0);
38
+ clip-path: inset(50%);
39
+ white-space: nowrap;
40
+ }
41
+
42
+ .cao-checkbox__control {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ flex: none;
47
+ box-sizing: border-box;
48
+ width: var(--cao-checkbox-size);
49
+ height: var(--cao-checkbox-size);
50
+ border: 2px solid var(--cao-checkbox-unselected);
51
+ border-radius: var(--cao-checkbox-radius);
52
+ background-color: transparent;
53
+ transition:
54
+ background-color var(--cao-checkbox-duration) ease,
55
+ border-color var(--cao-checkbox-duration) ease;
56
+ }
57
+
58
+ .cao-checkbox__icon {
59
+ flex: none;
60
+ width: var(--cao-checkbox-icon-size);
61
+ height: var(--cao-checkbox-icon-size);
62
+ color: var(--cao-checkbox-icon-color);
63
+ pointer-events: none;
64
+ object-fit: contain;
65
+ }
66
+
67
+ .cao-checkbox__icon--default {
68
+ fill: none;
69
+ stroke: currentColor;
70
+ stroke-width: 2.5;
71
+ stroke-linecap: round;
72
+ stroke-linejoin: round;
73
+ }
74
+
75
+ .cao-checkbox__label {
76
+ color: var(--cao-checkbox-label);
77
+ font-size: 0.875rem;
78
+ line-height: 1.25rem;
79
+ }
80
+
81
+ .cao-checkbox__label:empty {
82
+ display: none;
83
+ }
84
+
85
+ .cao-checkbox:not(.cao-checkbox--disabled):hover .cao-checkbox__control {
86
+ border-color: var(--cao-checkbox-color);
87
+ }
88
+
89
+ .cao-checkbox--checked .cao-checkbox__control,
90
+ .cao-checkbox--indeterminate .cao-checkbox__control {
91
+ border-color: var(--cao-checkbox-color);
92
+ background-color: var(--cao-checkbox-color);
93
+ }
94
+
95
+ .cao-checkbox--invalid .cao-checkbox__control {
96
+ border-color: var(--error, #dc2626);
97
+ }
98
+
99
+ .cao-checkbox--valid .cao-checkbox__control {
100
+ border-color: var(--success, #16a34a);
101
+ }
102
+
103
+ .cao-checkbox--disabled .cao-checkbox__control {
104
+ border-color: var(--cao-checkbox-disabled);
105
+ background-color: transparent;
106
+ }
107
+
108
+ .cao-checkbox--disabled.cao-checkbox--checked .cao-checkbox__control,
109
+ .cao-checkbox--disabled.cao-checkbox--indeterminate .cao-checkbox__control {
110
+ background-color: var(--cao-checkbox-disabled);
111
+ }
112
+
113
+ .cao-checkbox--disabled .cao-checkbox__icon {
114
+ opacity: 0.65;
115
+ }
116
+
117
+ .cao-checkbox--disabled .cao-checkbox__label {
118
+ color: var(--cao-checkbox-label-disabled);
119
+ }
120
+
121
+ .cao-checkbox__input:focus-visible + .cao-checkbox__control {
122
+ box-shadow: 0 0 0 3px var(--cao-checkbox-focus-ring);
123
+ }
124
+
125
+ .cao-checkbox--label-before .cao-checkbox__control {
126
+ order: 2;
127
+ }
128
+
129
+ .cao-checkbox--label-before .cao-checkbox__label {
130
+ order: 1;
131
+ }
@@ -0,0 +1,84 @@
1
+ <label
2
+ class="cao-checkbox"
3
+ [class.cao-checkbox--checked]="checked && !indeterminate"
4
+ [class.cao-checkbox--indeterminate]="indeterminate"
5
+ [class.cao-checkbox--disabled]="isDisabled"
6
+ [class.cao-checkbox--label-before]="labelPosition === 'before'"
7
+ [class.cao-checkbox--invalid]="hasInvalidState"
8
+ [class.cao-checkbox--valid]="hasValidState">
9
+ <input
10
+ #input
11
+ class="cao-checkbox__input"
12
+ type="checkbox"
13
+ [checked]="checked"
14
+ [indeterminate]="indeterminate"
15
+ [disabled]="isDisabled"
16
+ [required]="required"
17
+ [attr.name]="name || null"
18
+ [attr.id]="inputId"
19
+ [attr.aria-label]="ariaLabel || null"
20
+ [attr.aria-labelledby]="ariaLabelledBy || null"
21
+ [attr.data-cy]="dataCy || null"
22
+ (change)="onInputChange($event)"
23
+ (blur)="onBlur()" />
24
+
25
+ <span class="cao-checkbox__control" aria-hidden="true">
26
+ @if (indeterminate) {
27
+ @if (indeterminateIcon) {
28
+ @if (isLucideIcon) {
29
+ <svg
30
+ class="cao-checkbox__icon"
31
+ [lucideIcon]="indeterminateIcon"
32
+ [size]="iconSize"
33
+ [strokeWidth]="iconStrokeWidth"></svg>
34
+ } @else {
35
+ <img class="cao-checkbox__icon" alt="" [src]="indeterminateIcon" />
36
+ }
37
+ } @else {
38
+ <svg
39
+ class="cao-checkbox__icon cao-checkbox__icon--default"
40
+ viewBox="0 0 24 24"
41
+ aria-hidden="true">
42
+ <line x1="5" y1="12" x2="19" y2="12" />
43
+ </svg>
44
+ }
45
+ } @else if (checked) {
46
+ @if (checkedIcon) {
47
+ @if (isLucideIcon) {
48
+ <svg
49
+ class="cao-checkbox__icon"
50
+ [lucideIcon]="checkedIcon"
51
+ [size]="iconSize"
52
+ [strokeWidth]="iconStrokeWidth"></svg>
53
+ } @else {
54
+ <img class="cao-checkbox__icon" alt="" [src]="checkedIcon" />
55
+ }
56
+ } @else {
57
+ <svg
58
+ class="cao-checkbox__icon cao-checkbox__icon--default"
59
+ viewBox="0 0 24 24"
60
+ aria-hidden="true">
61
+ <polyline points="20 6 9 17 4 12" />
62
+ </svg>
63
+ }
64
+ } @else if (uncheckedIcon) {
65
+ @if (isLucideIcon) {
66
+ <svg
67
+ class="cao-checkbox__icon"
68
+ [lucideIcon]="uncheckedIcon"
69
+ [size]="iconSize"
70
+ [strokeWidth]="iconStrokeWidth"></svg>
71
+ } @else {
72
+ <img class="cao-checkbox__icon" alt="" [src]="uncheckedIcon" />
73
+ }
74
+ }
75
+ </span>
76
+
77
+ <span class="cao-checkbox__label">
78
+ @if (label) {
79
+ {{ label }}
80
+ } @else {
81
+ <ng-content />
82
+ }
83
+ </span>
84
+ </label>
@@ -0,0 +1,344 @@
1
+ import { Component, Input } from "@angular/core";
2
+ import { ComponentFixture, TestBed } from "@angular/core/testing";
3
+ import { FormControl, FormsModule, Validators } from "@angular/forms";
4
+ import { LucideCheck, LucideMinus, LucideX, provideLucideIcons } from "@lucide/angular";
5
+
6
+ import { CaoCheckboxComponent } from "./checkbox.component";
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [FormsModule, CaoCheckboxComponent],
11
+ template: `
12
+ <cao-checkbox
13
+ [(ngModel)]="checked"
14
+ [isDisabled]="isDisabled"
15
+ label="Aceito os termos"
16
+ name="terms"
17
+ dataCy="checkbox-terms"></cao-checkbox>
18
+ `,
19
+ })
20
+ class CheckboxHostComponent {
21
+ @Input() checked = false;
22
+ @Input() isDisabled = false;
23
+ }
24
+
25
+ describe("CaoCheckboxComponent", () => {
26
+ let component: CaoCheckboxComponent;
27
+ let fixture: ComponentFixture<CaoCheckboxComponent>;
28
+
29
+ beforeEach(async () => {
30
+ await TestBed.configureTestingModule({
31
+ imports: [CaoCheckboxComponent],
32
+ providers: [provideLucideIcons(LucideCheck, LucideMinus, LucideX)],
33
+ }).compileComponents();
34
+
35
+ fixture = TestBed.createComponent(CaoCheckboxComponent);
36
+ component = fixture.componentInstance;
37
+ fixture.detectChanges();
38
+ });
39
+
40
+ function getInput(): HTMLInputElement {
41
+ return fixture.nativeElement.querySelector("input");
42
+ }
43
+
44
+ function toggleInput(checked = true): void {
45
+ const input = getInput();
46
+ input.checked = checked;
47
+ input.dispatchEvent(new Event("change"));
48
+ fixture.detectChanges();
49
+ }
50
+
51
+ it("should create", () => {
52
+ expect(component).toBeTruthy();
53
+ });
54
+
55
+ it("should toggle and emit changes", () => {
56
+ const checkedChange = vi.fn();
57
+ const change = vi.fn();
58
+
59
+ component.checkedChange.subscribe(checkedChange);
60
+ component.checkboxChange.subscribe(change);
61
+
62
+ toggleInput(true);
63
+
64
+ expect(component.checked).toBe(true);
65
+ expect(checkedChange).toHaveBeenCalledWith(true);
66
+ expect(change).toHaveBeenCalledWith({
67
+ checked: true,
68
+ source: component,
69
+ });
70
+ });
71
+
72
+ it("should not toggle when disabled", () => {
73
+ component.isDisabled = true;
74
+ fixture.detectChanges();
75
+
76
+ toggleInput(true);
77
+
78
+ expect(component.checked).toBe(false);
79
+ expect(getInput().disabled).toBe(true);
80
+ });
81
+
82
+ it("should support ControlValueAccessor", () => {
83
+ const onChange = vi.fn();
84
+ const onTouched = vi.fn();
85
+
86
+ component.registerOnChange(onChange);
87
+ component.registerOnTouched(onTouched);
88
+ component.writeValue(true);
89
+
90
+ expect(component.checked).toBe(true);
91
+
92
+ component.onBlur();
93
+ expect(onTouched).toHaveBeenCalled();
94
+
95
+ component.toggle();
96
+ expect(onChange).toHaveBeenCalledWith(false);
97
+ });
98
+
99
+ it("should write null values as unchecked", () => {
100
+ component.writeValue(null);
101
+
102
+ expect(component.checked).toBe(false);
103
+ });
104
+
105
+ it("should enable and disable programmatically", () => {
106
+ component.disable();
107
+ expect(component.isDisabled).toBe(true);
108
+
109
+ component.enable();
110
+ expect(component.isDisabled).toBe(false);
111
+ });
112
+
113
+ it("should disable through setDisabledState", () => {
114
+ component.setDisabledState(true);
115
+ expect(component.isDisabled).toBe(true);
116
+
117
+ component.setDisabledState(false);
118
+ expect(component.isDisabled).toBe(false);
119
+ });
120
+
121
+ it("should clear indeterminate and check when clicked", () => {
122
+ const indeterminateChange = vi.fn();
123
+
124
+ component.indeterminate = true;
125
+ component.indeterminateChange.subscribe(indeterminateChange);
126
+ fixture.detectChanges();
127
+
128
+ toggleInput(true);
129
+
130
+ expect(component.checked).toBe(true);
131
+ expect(component.indeterminate).toBe(false);
132
+ expect(indeterminateChange).toHaveBeenCalledWith(false);
133
+ });
134
+
135
+ it("should set indeterminate programmatically", () => {
136
+ const indeterminateChange = vi.fn();
137
+
138
+ component.indeterminateChange.subscribe(indeterminateChange);
139
+ component.setIndeterminate(true);
140
+
141
+ expect(component.indeterminate).toBe(true);
142
+ expect(indeterminateChange).toHaveBeenCalledWith(true);
143
+ });
144
+
145
+ it("should render the default checked icon", () => {
146
+ fixture.componentRef.setInput("checked", true);
147
+ fixture.detectChanges();
148
+
149
+ expect(
150
+ fixture.nativeElement.querySelector(".cao-checkbox__icon--default"),
151
+ ).toBeTruthy();
152
+ });
153
+
154
+ it("should render the default indeterminate icon", () => {
155
+ fixture.componentRef.setInput("indeterminate", true);
156
+ fixture.detectChanges();
157
+
158
+ const icon = fixture.nativeElement.querySelector(
159
+ ".cao-checkbox__icon--default line",
160
+ );
161
+
162
+ expect(icon).toBeTruthy();
163
+ });
164
+
165
+ it("should render Lucide icons for every state", () => {
166
+ fixture.componentRef.setInput("checkedIcon", "check");
167
+ fixture.componentRef.setInput("uncheckedIcon", "x");
168
+ fixture.componentRef.setInput("indeterminateIcon", "minus");
169
+ fixture.componentRef.setInput("isLucideIcon", true);
170
+
171
+ fixture.componentRef.setInput("checked", true);
172
+ fixture.detectChanges();
173
+
174
+ expect(component.checkedIcon).toBe("check");
175
+ expect(
176
+ fixture.nativeElement.querySelector("svg.cao-checkbox__icon"),
177
+ ).toBeTruthy();
178
+ expect(
179
+ fixture.nativeElement.querySelector(".cao-checkbox__icon--default"),
180
+ ).toBeNull();
181
+
182
+ fixture.componentRef.setInput("checked", false);
183
+ fixture.detectChanges();
184
+
185
+ expect(
186
+ fixture.nativeElement.querySelector("svg.cao-checkbox__icon"),
187
+ ).toBeTruthy();
188
+
189
+ fixture.componentRef.setInput("indeterminate", true);
190
+ fixture.detectChanges();
191
+
192
+ expect(
193
+ fixture.nativeElement.querySelector("svg.cao-checkbox__icon"),
194
+ ).toBeTruthy();
195
+ });
196
+
197
+ it("should render an image when isLucideIcon is false", () => {
198
+ fixture.componentRef.setInput("checkedIcon", "icon.svg");
199
+ fixture.componentRef.setInput("checked", true);
200
+ fixture.detectChanges();
201
+
202
+ const image: HTMLImageElement = fixture.nativeElement.querySelector(
203
+ "img.cao-checkbox__icon",
204
+ );
205
+
206
+ expect(image).toBeTruthy();
207
+ expect(image.getAttribute("src")).toBe("icon.svg");
208
+ expect(
209
+ fixture.nativeElement.querySelector("svg.cao-checkbox__icon"),
210
+ ).toBeNull();
211
+ });
212
+
213
+ it("should not render an icon when unchecked and no icon is provided", () => {
214
+ fixture.detectChanges();
215
+
216
+ expect(
217
+ fixture.nativeElement.querySelector(
218
+ ".cao-checkbox__control svg, .cao-checkbox__control img",
219
+ ),
220
+ ).toBeNull();
221
+ });
222
+
223
+ it("should render the label and respect labelPosition", () => {
224
+ fixture.componentRef.setInput("label", "Aceito os termos");
225
+ fixture.detectChanges();
226
+
227
+ expect(
228
+ fixture.nativeElement
229
+ .querySelector(".cao-checkbox__label")
230
+ .textContent.trim(),
231
+ ).toBe("Aceito os termos");
232
+
233
+ fixture.componentRef.setInput("labelPosition", "before");
234
+ fixture.detectChanges();
235
+
236
+ expect(
237
+ fixture.nativeElement
238
+ .querySelector(".cao-checkbox")
239
+ .classList.contains("cao-checkbox--label-before"),
240
+ ).toBe(true);
241
+ });
242
+
243
+ it("should expose accessibility attributes", () => {
244
+ fixture.componentRef.setInput("ariaLabel", "Aceitar termos");
245
+ fixture.componentRef.setInput("required", true);
246
+ fixture.componentRef.setInput("name", "terms");
247
+ fixture.componentRef.setInput("dataCy", "checkbox-terms");
248
+ fixture.detectChanges();
249
+
250
+ const input = getInput();
251
+
252
+ expect(input.getAttribute("aria-label")).toBe("Aceitar termos");
253
+ expect(input.required).toBe(true);
254
+ expect(input.getAttribute("name")).toBe("terms");
255
+ expect(input.getAttribute("data-cy")).toBe("checkbox-terms");
256
+ });
257
+
258
+ it("should apply validation classes based on the form control", () => {
259
+ const control = new FormControl(false, Validators.requiredTrue);
260
+
261
+ fixture.componentRef.setInput("control", control);
262
+ control.markAsTouched();
263
+ fixture.detectChanges();
264
+
265
+ const label = fixture.nativeElement.querySelector(".cao-checkbox");
266
+
267
+ expect(label.classList.contains("cao-checkbox--invalid")).toBe(true);
268
+
269
+ control.setValue(true);
270
+ fixture.componentRef.changeDetectorRef.markForCheck();
271
+ fixture.detectChanges();
272
+
273
+ expect(label.classList.contains("cao-checkbox--valid")).toBe(true);
274
+ });
275
+
276
+ it("should expose custom colors, sizes and icon colors as CSS variables", () => {
277
+ fixture.componentRef.setInput("color", "#16a34a");
278
+ fixture.componentRef.setInput("uncheckedColor", "#94a3b8");
279
+ fixture.componentRef.setInput("labelColor", "#166534");
280
+ fixture.componentRef.setInput("focusRingColor", "rgba(22, 163, 74, 0.35)");
281
+ fixture.componentRef.setInput("size", "24px");
282
+ fixture.componentRef.setInput("borderRadius", "6px");
283
+ fixture.componentRef.setInput("animationDuration", "300ms");
284
+ fixture.componentRef.setInput("checkedIconColor", "#ffffff");
285
+ fixture.componentRef.setInput("iconSize", 16);
286
+ fixture.componentRef.setInput("checked", true);
287
+ fixture.detectChanges();
288
+
289
+ const host = fixture.nativeElement as HTMLElement;
290
+ const styles = host.style;
291
+
292
+ expect(styles.getPropertyValue("--cao-checkbox-color")).toBe("#16a34a");
293
+ expect(styles.getPropertyValue("--cao-checkbox-unselected")).toBe("#94a3b8");
294
+ expect(styles.getPropertyValue("--cao-checkbox-label")).toBe("#166534");
295
+ expect(styles.getPropertyValue("--cao-checkbox-size")).toBe("24px");
296
+ expect(styles.getPropertyValue("--cao-checkbox-radius")).toBe("6px");
297
+ expect(styles.getPropertyValue("--cao-checkbox-duration")).toBe("300ms");
298
+ expect(styles.getPropertyValue("--cao-checkbox-icon-size")).toBe("16px");
299
+ expect(styles.getPropertyValue("--cao-checkbox-icon-color")).toBe("#ffffff");
300
+ });
301
+ });
302
+
303
+ describe("CaoCheckboxComponent with ngModel", () => {
304
+ let fixture: ComponentFixture<CheckboxHostComponent>;
305
+
306
+ beforeEach(async () => {
307
+ await TestBed.configureTestingModule({
308
+ imports: [CheckboxHostComponent],
309
+ }).compileComponents();
310
+
311
+ fixture = TestBed.createComponent(CheckboxHostComponent);
312
+ fixture.detectChanges();
313
+ });
314
+
315
+ function getInput(): HTMLInputElement {
316
+ return fixture.nativeElement.querySelector("input");
317
+ }
318
+
319
+ it("should render the initial form value", async () => {
320
+ fixture.componentRef.setInput("checked", true);
321
+ fixture.detectChanges();
322
+ await fixture.whenStable();
323
+ fixture.detectChanges();
324
+
325
+ expect(getInput().checked).toBe(true);
326
+ });
327
+
328
+ it("should update the model when the input changes", () => {
329
+ const input = getInput();
330
+ input.checked = true;
331
+ input.dispatchEvent(new Event("change"));
332
+ fixture.detectChanges();
333
+
334
+ expect(fixture.componentInstance.checked).toBe(true);
335
+ });
336
+
337
+ it("should respect the disabled state", () => {
338
+ fixture.componentRef.setInput("isDisabled", true);
339
+ fixture.detectChanges();
340
+
341
+ expect(getInput().disabled).toBe(true);
342
+ });
343
+ });
344
+