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
@@ -0,0 +1,133 @@
1
+ import { CommonModule } from "@angular/common";
2
+ import {
3
+ Component,
4
+ forwardRef,
5
+ HostBinding,
6
+ Inject,
7
+ Input,
8
+ Optional,
9
+ } from "@angular/core";
10
+
11
+ import { CaoRadioGroupComponent } from "./radio-group.component";
12
+
13
+ let nextRadioId = 0;
14
+
15
+ @Component({
16
+ selector: "cao-radio-button",
17
+ standalone: true,
18
+ imports: [CommonModule],
19
+ templateUrl: "./radio-button.component.html",
20
+ styleUrl: "./radio-button.component.css",
21
+ })
22
+ export class CaoRadioButtonComponent {
23
+ @Input() value: unknown = undefined;
24
+ @Input() label = "";
25
+ @Input() isDisabled = false;
26
+ @Input() labelPosition?: "before" | "after";
27
+ @Input() id = "";
28
+ @Input() name = "";
29
+ @Input() ariaLabel = "";
30
+ @Input() ariaLabelledBy = "";
31
+ @Input() dataCy = "";
32
+
33
+ @Input() color?: string;
34
+ @Input() unselectedColor?: string;
35
+ @Input() disabledColor?: string;
36
+ @Input() labelColor?: string;
37
+ @Input() disabledLabelColor?: string;
38
+ @Input() focusRingColor?: string;
39
+ @Input() size?: string;
40
+ @Input() animationDuration?: string;
41
+
42
+ @Input() set disabled(value: boolean) {
43
+ this.isDisabled = value;
44
+ }
45
+
46
+ get disabled(): boolean {
47
+ return this.isDisabled;
48
+ }
49
+
50
+ private readonly uid = `cao-radio-${nextRadioId++}`;
51
+
52
+ constructor(
53
+ @Optional()
54
+ @Inject(forwardRef(() => CaoRadioGroupComponent))
55
+ readonly group: CaoRadioGroupComponent | null,
56
+ ) {}
57
+
58
+ get effectiveDisabled(): boolean {
59
+ return this.isDisabled || !!this.group?.isEffectivelyDisabled;
60
+ }
61
+
62
+ get effectiveLabelPosition(): "before" | "after" {
63
+ return this.labelPosition ?? this.group?.labelPosition ?? "after";
64
+ }
65
+
66
+ get effectiveName(): string {
67
+ return this.name || this.group?.groupName || this.uid;
68
+ }
69
+
70
+ get inputId(): string {
71
+ return this.id || this.uid;
72
+ }
73
+
74
+ get isSelected(): boolean {
75
+ return this.group?.isValueSelected(this.value) ?? false;
76
+ }
77
+
78
+ get isRequired(): boolean {
79
+ return this.group?.required ?? false;
80
+ }
81
+
82
+ @HostBinding("style")
83
+ get hostStyles(): Record<string, string> {
84
+ const styles: Record<string, string> = {};
85
+
86
+ if (this.color) {
87
+ styles["--cao-radio-color"] = this.color;
88
+ }
89
+ if (this.unselectedColor) {
90
+ styles["--cao-radio-unselected"] = this.unselectedColor;
91
+ }
92
+ if (this.disabledColor) {
93
+ styles["--cao-radio-disabled"] = this.disabledColor;
94
+ }
95
+ if (this.labelColor) {
96
+ styles["--cao-radio-label"] = this.labelColor;
97
+ }
98
+ if (this.disabledLabelColor) {
99
+ styles["--cao-radio-label-disabled"] = this.disabledLabelColor;
100
+ }
101
+ if (this.focusRingColor) {
102
+ styles["--cao-radio-focus-ring"] = this.focusRingColor;
103
+ }
104
+ if (this.size) {
105
+ styles["--cao-radio-size"] = this.size;
106
+ }
107
+ if (this.animationDuration) {
108
+ styles["--cao-radio-duration"] = this.animationDuration;
109
+ }
110
+
111
+ return styles;
112
+ }
113
+
114
+ select(): void {
115
+ if (this.effectiveDisabled) {
116
+ return;
117
+ }
118
+
119
+ this.group?.select(this);
120
+ }
121
+
122
+ onBlur(): void {
123
+ this.group?.markAsTouched();
124
+ }
125
+
126
+ enable(): void {
127
+ this.isDisabled = false;
128
+ }
129
+
130
+ disable(): void {
131
+ this.isDisabled = true;
132
+ }
133
+ }
@@ -0,0 +1,94 @@
1
+ import { FormsModule } from '@angular/forms';
2
+
3
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
4
+
5
+ import { CaoRadioButtonComponent } from './radio-button.component';
6
+ import { CaoRadioGroupComponent } from './radio-group.component';
7
+
8
+ const meta: Meta<CaoRadioButtonComponent> = {
9
+ title: 'Componentes/Radio Button',
10
+ component: CaoRadioButtonComponent,
11
+ tags: ['autodocs'],
12
+ decorators: [
13
+ (story) => ({
14
+ ...story,
15
+ moduleMetadata: {
16
+ imports: [FormsModule, CaoRadioGroupComponent, CaoRadioButtonComponent],
17
+ },
18
+ }),
19
+ ],
20
+ argTypes: {
21
+ labelPosition: { control: 'select', options: ['before', 'after'] },
22
+ isDisabled: { control: 'boolean' },
23
+ disabled: { control: 'boolean' },
24
+ color: { control: 'color' },
25
+ unselectedColor: { control: 'color' },
26
+ disabledColor: { control: 'color' },
27
+ labelColor: { control: 'color' },
28
+ disabledLabelColor: { control: 'color' },
29
+ focusRingColor: { control: 'color' },
30
+ size: { control: 'text' },
31
+ animationDuration: { control: 'text' },
32
+ },
33
+ args: {
34
+ value: 'option-1',
35
+ label: 'Opção selecionada',
36
+ labelPosition: 'after',
37
+ isDisabled: false,
38
+ },
39
+ render: (args) => ({
40
+ props: {
41
+ ...args,
42
+ selectedValue: 'option-1',
43
+ },
44
+ template: `
45
+ <cao-radio-group
46
+ [(ngModel)]="selectedValue"
47
+ [labelPosition]="labelPosition">
48
+ <cao-radio-button
49
+ value="option-1"
50
+ [label]="label"
51
+ [isDisabled]="isDisabled"
52
+ [labelPosition]="labelPosition"
53
+ [color]="color"
54
+ [unselectedColor]="unselectedColor"
55
+ [disabledColor]="disabledColor"
56
+ [labelColor]="labelColor"
57
+ [disabledLabelColor]="disabledLabelColor"
58
+ [focusRingColor]="focusRingColor"
59
+ [size]="size"
60
+ [animationDuration]="animationDuration" />
61
+ <cao-radio-button value="option-2" label="Outra opção" />
62
+ </cao-radio-group>
63
+ <p>Selecionado: {{ selectedValue }}</p>
64
+ `,
65
+ }),
66
+ };
67
+
68
+ export default meta;
69
+ type Story = StoryObj<CaoRadioButtonComponent>;
70
+
71
+ export const Padrao: Story = {};
72
+
73
+ export const Desabilitado: Story = {
74
+ args: {
75
+ isDisabled: true,
76
+ label: 'Opção desabilitada',
77
+ },
78
+ };
79
+
80
+ export const CoresCustomizadas: Story = {
81
+ args: {
82
+ color: '#16a34a',
83
+ unselectedColor: '#94a3b8',
84
+ labelColor: '#166534',
85
+ size: '24px',
86
+ },
87
+ };
88
+
89
+ export const LabelAntes: Story = {
90
+ args: {
91
+ labelPosition: 'before',
92
+ label: 'Label antes',
93
+ },
94
+ };
@@ -0,0 +1,21 @@
1
+ :host {
2
+ --cao-radio-color: #4f46e5;
3
+ --cao-radio-unselected: #6b7280;
4
+ --cao-radio-disabled: #d1d5db;
5
+ --cao-radio-label: #111827;
6
+ --cao-radio-label-disabled: #9ca3af;
7
+ --cao-radio-focus-ring: rgba(79, 70, 229, 0.35);
8
+ --cao-radio-size: 20px;
9
+ --cao-radio-duration: 200ms;
10
+ --cao-radio-gap: 8px;
11
+
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--cao-radio-gap);
15
+ }
16
+
17
+ :host(.cao-radio-group--horizontal) {
18
+ flex-direction: row;
19
+ flex-wrap: wrap;
20
+ align-items: center;
21
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ ChangeDetectorRef,
4
+ Component,
5
+ EventEmitter,
6
+ forwardRef,
7
+ HostBinding,
8
+ inject,
9
+ Input,
10
+ Output,
11
+ } from "@angular/core";
12
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
13
+
14
+ import type { CaoRadioButtonComponent } from "./radio-button.component";
15
+
16
+ let nextRadioGroupId = 0;
17
+
18
+ export interface ICaoRadioChangeEvent {
19
+ value: unknown;
20
+ source: CaoRadioButtonComponent;
21
+ }
22
+
23
+ @Component({
24
+ selector: "cao-radio-group",
25
+ standalone: true,
26
+ template: "<ng-content />",
27
+ styleUrl: "./radio-group.component.css",
28
+ providers: [
29
+ {
30
+ provide: NG_VALUE_ACCESSOR,
31
+ useExisting: forwardRef(() => CaoRadioGroupComponent),
32
+ multi: true,
33
+ },
34
+ ],
35
+ changeDetection: ChangeDetectionStrategy.OnPush,
36
+ })
37
+ export class CaoRadioGroupComponent implements ControlValueAccessor {
38
+ @Input() value: unknown = undefined;
39
+ @Input() name = "";
40
+ @Input() isDisabled = false;
41
+ @Input() required = false;
42
+ @Input() orientation: "vertical" | "horizontal" = "vertical";
43
+ @Input() labelPosition: "before" | "after" = "after";
44
+ @Input() compareWith: (option: unknown, value: unknown) => boolean = (
45
+ option,
46
+ value,
47
+ ) => option === value;
48
+
49
+ @Input() color?: string;
50
+ @Input() unselectedColor?: string;
51
+ @Input() disabledColor?: string;
52
+ @Input() labelColor?: string;
53
+ @Input() disabledLabelColor?: string;
54
+ @Input() focusRingColor?: string;
55
+ @Input() size?: string;
56
+ @Input() animationDuration?: string;
57
+ @Input() gap?: string;
58
+
59
+ @Input() ariaLabel = "";
60
+ @Input() ariaLabelledBy = "";
61
+ @Input() dataCy = "";
62
+
63
+ @Input() set disabled(value: boolean) {
64
+ this.isDisabled = value;
65
+ this.cdr.markForCheck();
66
+ }
67
+
68
+ get disabled(): boolean {
69
+ return this.isDisabled;
70
+ }
71
+
72
+ @Output() readonly valueChange = new EventEmitter<unknown>();
73
+ @Output() readonly selectionChange = new EventEmitter<ICaoRadioChangeEvent>();
74
+
75
+ readonly resolvedName = `cao-radio-group-${nextRadioGroupId++}`;
76
+
77
+ private readonly cdr = inject(ChangeDetectorRef);
78
+ private formDisabled = false;
79
+ private onChange: (value: unknown) => void = () => {};
80
+ private onTouched: () => void = () => {};
81
+
82
+ get isEffectivelyDisabled(): boolean {
83
+ return this.isDisabled || this.formDisabled;
84
+ }
85
+
86
+ @HostBinding("attr.role")
87
+ readonly role = "radiogroup";
88
+
89
+ @HostBinding("class.cao-radio-group--horizontal")
90
+ get isHorizontal(): boolean {
91
+ return this.orientation === "horizontal";
92
+ }
93
+
94
+ @HostBinding("class.cao-radio-group--disabled")
95
+ get isDisabledClass(): boolean {
96
+ return this.isEffectivelyDisabled;
97
+ }
98
+
99
+ @HostBinding("attr.aria-label")
100
+ get ariaLabelAttr(): string | null {
101
+ return this.ariaLabel || null;
102
+ }
103
+
104
+ @HostBinding("attr.aria-labelledby")
105
+ get ariaLabelledByAttr(): string | null {
106
+ return this.ariaLabelledBy || null;
107
+ }
108
+
109
+ @HostBinding("attr.aria-disabled")
110
+ get ariaDisabledAttr(): boolean | null {
111
+ return this.isEffectivelyDisabled || null;
112
+ }
113
+
114
+ @HostBinding("attr.aria-required")
115
+ get ariaRequiredAttr(): boolean | null {
116
+ return this.required || null;
117
+ }
118
+
119
+ @HostBinding("attr.data-cy")
120
+ get dataCyAttr(): string | null {
121
+ return this.dataCy || null;
122
+ }
123
+
124
+ @HostBinding("style")
125
+ get hostStyles(): Record<string, string> {
126
+ const styles: Record<string, string> = {};
127
+
128
+ if (this.color) {
129
+ styles["--cao-radio-color"] = this.color;
130
+ }
131
+ if (this.unselectedColor) {
132
+ styles["--cao-radio-unselected"] = this.unselectedColor;
133
+ }
134
+ if (this.disabledColor) {
135
+ styles["--cao-radio-disabled"] = this.disabledColor;
136
+ }
137
+ if (this.labelColor) {
138
+ styles["--cao-radio-label"] = this.labelColor;
139
+ }
140
+ if (this.disabledLabelColor) {
141
+ styles["--cao-radio-label-disabled"] = this.disabledLabelColor;
142
+ }
143
+ if (this.focusRingColor) {
144
+ styles["--cao-radio-focus-ring"] = this.focusRingColor;
145
+ }
146
+ if (this.size) {
147
+ styles["--cao-radio-size"] = this.size;
148
+ }
149
+ if (this.animationDuration) {
150
+ styles["--cao-radio-duration"] = this.animationDuration;
151
+ }
152
+ if (this.gap) {
153
+ styles["--cao-radio-gap"] = this.gap;
154
+ }
155
+
156
+ return styles;
157
+ }
158
+
159
+ get groupName(): string {
160
+ return this.name || this.resolvedName;
161
+ }
162
+
163
+ isValueSelected(value: unknown): boolean {
164
+ return this.compareWith(this.value, value);
165
+ }
166
+
167
+ select(button: CaoRadioButtonComponent): void {
168
+ if (this.isEffectivelyDisabled || button.effectiveDisabled) {
169
+ return;
170
+ }
171
+
172
+ if (this.compareWith(this.value, button.value)) {
173
+ this.markAsTouched();
174
+ return;
175
+ }
176
+
177
+ this.value = button.value;
178
+ this.onChange(this.value);
179
+ this.valueChange.emit(this.value);
180
+ this.selectionChange.emit({ value: this.value, source: button });
181
+ this.markAsTouched();
182
+ this.cdr.markForCheck();
183
+ }
184
+
185
+ markAsTouched(): void {
186
+ this.onTouched();
187
+ }
188
+
189
+ enable(): void {
190
+ this.isDisabled = false;
191
+ this.cdr.markForCheck();
192
+ }
193
+
194
+ disable(): void {
195
+ this.isDisabled = true;
196
+ this.cdr.markForCheck();
197
+ }
198
+
199
+ writeValue(value: unknown): void {
200
+ this.value = value;
201
+ this.cdr.markForCheck();
202
+ }
203
+
204
+ registerOnChange(fn: (value: unknown) => void): void {
205
+ this.onChange = fn;
206
+ }
207
+
208
+ registerOnTouched(fn: () => void): void {
209
+ this.onTouched = fn;
210
+ }
211
+
212
+ setDisabledState(isDisabled: boolean): void {
213
+ this.formDisabled = isDisabled;
214
+ this.cdr.markForCheck();
215
+ }
216
+ }
@@ -0,0 +1,128 @@
1
+ import { Component, Input } from "@angular/core";
2
+ import { ComponentFixture, TestBed } from "@angular/core/testing";
3
+ import { FormsModule } from "@angular/forms";
4
+
5
+ import { CaoRadioButtonComponent } from "./radio-button.component";
6
+ import { CaoRadioGroupComponent } from "./radio-group.component";
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [FormsModule, CaoRadioGroupComponent, CaoRadioButtonComponent],
11
+ template: `
12
+ <cao-radio-group
13
+ [(ngModel)]="value"
14
+ [isDisabled]="isDisabled"
15
+ ariaLabel="Opções">
16
+ <cao-radio-button value="option-1" label="Opção 1" />
17
+ <cao-radio-button value="option-2" label="Opção 2" />
18
+ <cao-radio-button
19
+ value="option-3"
20
+ label="Opção 3"
21
+ [isDisabled]="isOptionDisabled" />
22
+ </cao-radio-group>
23
+ `,
24
+ })
25
+ class RadioHostComponent {
26
+ @Input() value = "option-1";
27
+ @Input() isDisabled = false;
28
+ @Input() isOptionDisabled = false;
29
+ }
30
+
31
+ describe("CaoRadioGroupComponent", () => {
32
+ let fixture: ComponentFixture<RadioHostComponent>;
33
+ let host: RadioHostComponent;
34
+
35
+ beforeEach(async () => {
36
+ await TestBed.configureTestingModule({
37
+ imports: [RadioHostComponent],
38
+ }).compileComponents();
39
+
40
+ fixture = TestBed.createComponent(RadioHostComponent);
41
+ host = fixture.componentInstance;
42
+ fixture.detectChanges();
43
+ await fixture.whenStable();
44
+ fixture.changeDetectorRef.detectChanges();
45
+ });
46
+
47
+ function getInputs(): HTMLInputElement[] {
48
+ return Array.from(fixture.nativeElement.querySelectorAll("input"));
49
+ }
50
+
51
+ function select(input: HTMLInputElement): void {
52
+ input.checked = true;
53
+ input.dispatchEvent(new Event("change"));
54
+ fixture.detectChanges();
55
+ }
56
+
57
+ it("should create the group and radio buttons", () => {
58
+ expect(fixture.nativeElement.querySelector("cao-radio-group")).toBeTruthy();
59
+ expect(fixture.nativeElement.querySelectorAll("cao-radio-button").length).toBe(3);
60
+ });
61
+
62
+ it("should reflect the initial value", () => {
63
+ const inputs = getInputs();
64
+
65
+ expect(inputs[0].checked).toBe(true);
66
+ expect(inputs[1].checked).toBe(false);
67
+ });
68
+
69
+ it("should select an option and emit the value", () => {
70
+ const group = fixture.debugElement.query(
71
+ (element) => element.componentInstance instanceof CaoRadioGroupComponent,
72
+ ).componentInstance as CaoRadioGroupComponent;
73
+ const valueChange = vi.fn();
74
+ const selectionChange = vi.fn();
75
+
76
+ group.valueChange.subscribe(valueChange);
77
+ group.selectionChange.subscribe(selectionChange);
78
+
79
+ select(getInputs()[1]);
80
+
81
+ expect(host.value).toBe("option-2");
82
+ expect(valueChange).toHaveBeenCalledWith("option-2");
83
+ expect(selectionChange).toHaveBeenCalledWith(
84
+ expect.objectContaining({ value: "option-2" }),
85
+ );
86
+ });
87
+
88
+ it("should disable every option when the group is disabled", () => {
89
+ fixture.componentRef.setInput("isDisabled", true);
90
+ fixture.detectChanges();
91
+
92
+ const inputs = getInputs();
93
+ expect(inputs.every((input) => input.disabled)).toBe(true);
94
+
95
+ select(inputs[1]);
96
+ expect(host.value).toBe("option-1");
97
+ });
98
+
99
+ it("should not select an individually disabled option", () => {
100
+ fixture.componentRef.setInput("isOptionDisabled", true);
101
+ fixture.detectChanges();
102
+
103
+ const inputs = getInputs();
104
+ expect(inputs[2].disabled).toBe(true);
105
+
106
+ select(inputs[2]);
107
+ expect(host.value).toBe("option-1");
108
+ });
109
+
110
+ it("should expose radiogroup accessibility attributes", () => {
111
+ const group: HTMLElement =
112
+ fixture.nativeElement.querySelector("cao-radio-group");
113
+
114
+ expect(group.getAttribute("role")).toBe("radiogroup");
115
+ expect(group.getAttribute("aria-label")).toBe("Opções");
116
+ });
117
+
118
+ it("should support ControlValueAccessor on the group", () => {
119
+ const group = fixture.debugElement.query(
120
+ (element) => element.componentInstance instanceof CaoRadioGroupComponent,
121
+ ).componentInstance as CaoRadioGroupComponent;
122
+
123
+ group.writeValue("option-2");
124
+ fixture.detectChanges();
125
+
126
+ expect(getInputs()[1].checked).toBe(true);
127
+ });
128
+ });