sn-checkbox-x 0.0.1

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 ADDED
@@ -0,0 +1,189 @@
1
+ # sn-checkbox
2
+
3
+ A customizable and accessible checkbox component for Angular applications.
4
+
5
+ ## Overview
6
+
7
+ The `sn-checkbox` component provides a modern, styled checkbox input with support for:
8
+
9
+ - ✅ Standard checked/unchecked states
10
+ - ✅ Indeterminate state (for multi-select patterns)
11
+ - ✅ Disabled state
12
+ - ✅ Custom labels
13
+ - ✅ Form control integration (ControlValueAccessor)
14
+ - ✅ Full accessibility support (ARIA labels)
15
+ - ✅ Keyboard navigation
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install sn-checkbox
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Basic Checkbox
26
+
27
+ ```typescript
28
+ import { Component } from '@angular/core';
29
+ import { SnCheckboxComponent } from 'sn-checkbox';
30
+
31
+ @Component({
32
+ selector: 'app-demo',
33
+ template: `
34
+ <sn-checkbox
35
+ id="agree"
36
+ label="I agree to the terms"
37
+ (changed)="onChecked($event)"
38
+ ></sn-checkbox>
39
+ `,
40
+ imports: [SnCheckboxComponent]
41
+ })
42
+ export class DemoComponent {
43
+ onChecked(value: boolean): void {
44
+ console.log('Checkbox value:', value);
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### With Reactive Forms
50
+
51
+ ```typescript
52
+ import { Component } from '@angular/core';
53
+ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
54
+ import { SnCheckboxComponent } from 'sn-checkbox';
55
+
56
+ @Component({
57
+ selector: 'app-form',
58
+ template: `
59
+ <form [formGroup]="form">
60
+ <sn-checkbox
61
+ id="newsletter"
62
+ label="Subscribe to newsletter"
63
+ formControlName="newsletter"
64
+ ></sn-checkbox>
65
+ </form>
66
+ `,
67
+ imports: [SnCheckboxComponent, ReactiveFormsModule]
68
+ })
69
+ export class FormComponent {
70
+ form: FormGroup;
71
+
72
+ constructor(fb: FormBuilder) {
73
+ this.form = fb.group({
74
+ newsletter: [false]
75
+ });
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### With Template Forms
81
+
82
+ ```typescript
83
+ import { Component } from '@angular/core';
84
+ import { FormsModule } from '@angular/forms';
85
+ import { SnCheckboxComponent } from 'sn-checkbox';
86
+
87
+ @Component({
88
+ selector: 'app-template-form',
89
+ template: `
90
+ <sn-checkbox
91
+ id="terms"
92
+ [(ngModel)]="accepted"
93
+ label="I accept"
94
+ ></sn-checkbox>
95
+ `,
96
+ imports: [SnCheckboxComponent, FormsModule]
97
+ })
98
+ export class TemplateFormComponent {
99
+ accepted: boolean = false;
100
+ }
101
+ ```
102
+
103
+ ### Indeterminate State
104
+
105
+ ```typescript
106
+ <sn-checkbox
107
+ id="select-all"
108
+ [indeterminate]="someSelected && !allSelected"
109
+ [checked]="allSelected"
110
+ label="Select All"
111
+ ></sn-checkbox>
112
+ ```
113
+
114
+ ### Disabled State
115
+
116
+ ```typescript
117
+ <sn-checkbox
118
+ id="disabled-check"
119
+ [disabled]="true"
120
+ label="Disabled checkbox"
121
+ ></sn-checkbox>
122
+ ```
123
+
124
+ ## API
125
+
126
+ ### Inputs
127
+
128
+ | Input | Type | Default | Description |
129
+ |-------|------|---------|-------------|
130
+ | `id` | `string` | `''` | HTML id attribute for the checkbox |
131
+ | `label` | `string` | `''` | Label text displayed next to the checkbox |
132
+ | `name` | `string` | `''` | HTML name attribute for the checkbox |
133
+ | `disabled` | `boolean` | `false` | Whether the checkbox is disabled |
134
+ | `indeterminate` | `boolean` | `false` | Whether the checkbox is in indeterminate state |
135
+
136
+ ### Outputs
137
+
138
+ | Output | Type | Description |
139
+ |--------|------|-------------|
140
+ | `changed` | `EventEmitter<boolean>` | Emitted when checkbox state changes |
141
+
142
+ ### Methods
143
+
144
+ The component implements `ControlValueAccessor` for seamless integration with Angular forms.
145
+
146
+ ## Styling
147
+
148
+ The component uses Tailwind CSS for styling. It includes:
149
+
150
+ - Blue color scheme (customizable via CSS variables)
151
+ - Smooth transitions and hover effects
152
+ - Focus states with shadow effects
153
+ - Disabled state styling
154
+ - Dark and light mode compatible
155
+
156
+ ## Testing
157
+
158
+ ```bash
159
+ ng test sn-checkbox
160
+ ```
161
+
162
+ ## Building
163
+
164
+ ```bash
165
+ ng build sn-checkbox
166
+ ```
167
+
168
+ ## Accessibility
169
+
170
+ The component includes:
171
+
172
+ - Proper ARIA labels for screen readers
173
+ - Keyboard navigation support
174
+ - Focus indicators
175
+ - Semantic HTML structure
176
+ - Support for indeterminate state indication
177
+ ## Running end-to-end tests
178
+
179
+ For end-to-end (e2e) testing, run:
180
+
181
+ ```bash
182
+ ng e2e
183
+ ```
184
+
185
+ Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
186
+
187
+ ## Additional Resources
188
+
189
+ For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/sn-checkbox",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "sn-checkbox-x",
3
+ "version": "0.0.1",
4
+ "description": "Angular checkbox component with indeterminate state support - SnUI Library",
5
+ "author": {
6
+ "name": "Swapnil Nakate",
7
+ "email": "nakate.swapnil7@gmail.com",
8
+ "url": "https://swapnilnakate.in"
9
+ },
10
+ "keywords": [
11
+ "checkbox",
12
+ "angular",
13
+ "form-control",
14
+ "indeterminate",
15
+ "tailwindcss",
16
+ "accessible",
17
+ "aria",
18
+ "snui"
19
+ ],
20
+ "peerDependencies": {
21
+ "@angular/common": "^21.2.0",
22
+ "@angular/core": "^21.2.0"
23
+ },
24
+ "dependencies": {
25
+ "tslib": "^2.3.0"
26
+ },
27
+ "sideEffects": false,
28
+ "bugs": {
29
+ "url": "https://github.com/swapnilnakate7/sn-ui/issues",
30
+ "email": "nakate.swapnil7@gmail.com"
31
+ },
32
+ "repository": {
33
+ "url": "https://github.com/swapnilnakate7/sn-ui",
34
+ "type": "git",
35
+ "directory": "projects/sn-checkbox"
36
+ },
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,20 @@
1
+ <div class="sn-checkbox-wrapper">
2
+ <input
3
+ type="checkbox"
4
+ class="sn-checkbox-input"
5
+ [id]="id"
6
+ [name]="name"
7
+ [checked]="checked"
8
+ [disabled]="disabled"
9
+ [indeterminate]="indeterminate"
10
+ (change)="onCheckboxChange($event)"
11
+ (blur)="onBlur()"
12
+ aria-role="checkbox"
13
+ [attr.aria-checked]="indeterminate ? 'mixed' : checked"
14
+ [attr.aria-label]="label"
15
+ />
16
+ <label class="sn-checkbox-label" [attr.for]="id" *ngIf="label">
17
+ {{ label }}
18
+ </label>
19
+ </div>
20
+
@@ -0,0 +1,96 @@
1
+ .sn-checkbox-wrapper {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
5
+ user-select: none;
6
+ }
7
+
8
+ .sn-checkbox-input {
9
+ width: 1.25rem;
10
+ height: 1.25rem;
11
+ cursor: pointer;
12
+ appearance: none;
13
+ border: 2px solid #d1d5db;
14
+ border-radius: 0.375rem;
15
+ background-color: #ffffff;
16
+ transition: all 0.2s ease-in-out;
17
+
18
+ &:hover:not(:disabled):not(:checked) {
19
+ border-color: #3b82f6;
20
+ background-color: #f0f9ff;
21
+ }
22
+
23
+ &:checked {
24
+ background-color: #3b82f6;
25
+ border-color: #3b82f6;
26
+ position: relative;
27
+
28
+ &:hover:not(:disabled) {
29
+ background-color: #2563eb;
30
+ }
31
+
32
+ &::after {
33
+ content: '✓';
34
+ position: absolute;
35
+ top: 50%;
36
+ left: 50%;
37
+ transform: translate(-50%, -50%);
38
+ color: white;
39
+ font-size: 0.875rem;
40
+ font-weight: bold;
41
+ }
42
+ }
43
+
44
+ &:indeterminate {
45
+ background-color: #3b82f6;
46
+ border-color: #3b82f6;
47
+ position: relative;
48
+
49
+ &::after {
50
+ content: '−';
51
+ position: absolute;
52
+ top: 50%;
53
+ left: 50%;
54
+ transform: translate(-50%, -50%);
55
+ color: white;
56
+ font-size: 1rem;
57
+ font-weight: bold;
58
+ }
59
+ }
60
+
61
+ &:focus {
62
+ outline: none;
63
+ border-color: #3b82f6;
64
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
65
+ }
66
+
67
+ &:disabled {
68
+ cursor: not-allowed;
69
+ opacity: 0.5;
70
+ background-color: #f3f4f6;
71
+ border-color: #e5e7eb;
72
+ }
73
+
74
+ &:disabled:checked {
75
+ background-color: #9ca3af;
76
+ border-color: #9ca3af;
77
+ }
78
+ }
79
+
80
+ .sn-checkbox-label {
81
+ cursor: pointer;
82
+ font-size: 0.875rem;
83
+ line-height: 1.25rem;
84
+ color: #374151;
85
+ transition: color 0.2s ease-in-out;
86
+
87
+ .sn-checkbox-input:disabled ~ & {
88
+ opacity: 0.5;
89
+ cursor: not-allowed;
90
+ }
91
+
92
+ &:hover {
93
+ color: #1f2937;
94
+ }
95
+ }
96
+
@@ -0,0 +1,96 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { SnCheckboxXComponent } from './sn-checkbox-x';
3
+ import { DebugElement } from '@angular/core';
4
+ import { By } from '@angular/platform-browser';
5
+
6
+ describe('SnCheckboxXComponent', () => {
7
+ let component: SnCheckboxXComponent;
8
+ let fixture: ComponentFixture<SnCheckboxXComponent>;
9
+ let inputElement: DebugElement;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ imports: [SnCheckboxXComponent],
14
+ }).compileComponents();
15
+
16
+ fixture = TestBed.createComponent(SnCheckboxXComponent);
17
+ component = fixture.componentInstance;
18
+ fixture.detectChanges();
19
+ inputElement = fixture.debugElement.query(By.css('input[type="checkbox"]'));
20
+ });
21
+
22
+ it('should create', () => {
23
+ expect(component).toBeTruthy();
24
+ });
25
+
26
+ it('should toggle checkbox value on change', () => {
27
+ const spy = jasmine.createSpy('onChange');
28
+ component.registerOnChange(spy);
29
+
30
+ inputElement.nativeElement.click();
31
+ fixture.detectChanges();
32
+
33
+ expect(component.checked).toBe(true);
34
+ expect(spy).toHaveBeenCalledWith(true);
35
+ });
36
+
37
+ it('should emit changed event on checkbox change', () => {
38
+ const spy = spyOn(component.changed, 'emit');
39
+
40
+ inputElement.nativeElement.click();
41
+ fixture.detectChanges();
42
+
43
+ expect(spy).toHaveBeenCalledWith(true);
44
+ });
45
+
46
+ it('should update checked value from writeValue', () => {
47
+ component.writeValue(true);
48
+ fixture.detectChanges();
49
+
50
+ expect(component.checked).toBe(true);
51
+ });
52
+
53
+ it('should handle disabled state', () => {
54
+ component.setDisabledState(true);
55
+ fixture.detectChanges();
56
+
57
+ expect(component.disabled).toBe(true);
58
+ expect(inputElement.nativeElement.disabled).toBe(true);
59
+ });
60
+
61
+ it('should set indeterminate state', () => {
62
+ component.indeterminate = true;
63
+ fixture.detectChanges();
64
+
65
+ expect(inputElement.nativeElement.indeterminate).toBe(true);
66
+ });
67
+
68
+ it('should clear indeterminate state on checkbox change', () => {
69
+ component.indeterminate = true;
70
+ inputElement.nativeElement.click();
71
+ fixture.detectChanges();
72
+
73
+ expect(component.indeterminate).toBe(false);
74
+ });
75
+
76
+ it('should display label when provided', () => {
77
+ component.label = 'Accept Terms';
78
+ component.id = 'accept-terms';
79
+ fixture.detectChanges();
80
+
81
+ const labelElement = fixture.debugElement.query(By.css('label'));
82
+ expect(labelElement).toBeTruthy();
83
+ expect(labelElement.nativeElement.textContent).toContain('Accept Terms');
84
+ expect(labelElement.nativeElement.getAttribute('for')).toBe('accept-terms');
85
+ });
86
+
87
+ it('should call onTouched on blur', () => {
88
+ const spy = jasmine.createSpy('onTouched');
89
+ component.registerOnTouched(spy);
90
+
91
+ inputElement.nativeElement.dispatchEvent(new Event('blur'));
92
+ fixture.detectChanges();
93
+
94
+ expect(spy).toHaveBeenCalled();
95
+ });
96
+ });
@@ -0,0 +1,59 @@
1
+ import { Component, Input, Output, EventEmitter, forwardRef, booleanAttribute } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
4
+
5
+ @Component({
6
+ selector: 'sn-checkbox-x',
7
+ standalone: true,
8
+ imports: [CommonModule],
9
+ templateUrl: './sn-checkbox-x.html',
10
+ styleUrl: './sn-checkbox-x.scss',
11
+ providers: [
12
+ {
13
+ provide: NG_VALUE_ACCESSOR,
14
+ useExisting: forwardRef(() => SnCheckboxXComponent),
15
+ multi: true,
16
+ },
17
+ ],
18
+ })
19
+ export class SnCheckboxXComponent implements ControlValueAccessor {
20
+ @Input() id: string = '';
21
+ @Input() label: string = '';
22
+ @Input() name: string = '';
23
+ @Input({ transform: booleanAttribute }) disabled: boolean = false;
24
+ @Input({ transform: booleanAttribute }) indeterminate: boolean = false;
25
+ @Output() changed = new EventEmitter<boolean>();
26
+
27
+ checked: boolean = false;
28
+ private onChange: (value: boolean) => void = () => {};
29
+ private onTouched: () => void = () => {};
30
+
31
+ onCheckboxChange(event: Event): void {
32
+ const target = event.target as HTMLInputElement;
33
+ this.checked = target.checked;
34
+ this.indeterminate = false;
35
+ this.onChange(this.checked);
36
+ this.changed.emit(this.checked);
37
+ }
38
+
39
+ onBlur(): void {
40
+ this.onTouched();
41
+ }
42
+
43
+ // ControlValueAccessor implementation
44
+ writeValue(value: boolean): void {
45
+ this.checked = value ?? false;
46
+ }
47
+
48
+ registerOnChange(fn: (value: boolean) => void): void {
49
+ this.onChange = fn;
50
+ }
51
+
52
+ registerOnTouched(fn: () => void): void {
53
+ this.onTouched = fn;
54
+ }
55
+
56
+ setDisabledState(isDisabled: boolean): void {
57
+ this.disabled = isDisabled;
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Public API Surface of sn-checkbox-x
3
+ */
4
+
5
+ export * from './lib/sn-checkbox-x';
@@ -0,0 +1,18 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "../../tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "../../out-tsc/lib",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "inlineSources": true,
10
+ "types": []
11
+ },
12
+ "include": [
13
+ "src/**/*.ts"
14
+ ],
15
+ "exclude": [
16
+ "**/*.spec.ts"
17
+ ]
18
+ }
@@ -0,0 +1,11 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.lib.json",
5
+ "compilerOptions": {
6
+ "declarationMap": false
7
+ },
8
+ "angularCompilerOptions": {
9
+ "compilationMode": "partial"
10
+ }
11
+ }
@@ -0,0 +1,14 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "../../tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "../../out-tsc/spec",
7
+ "types": [
8
+ "jasmine"
9
+ ]
10
+ },
11
+ "include": [
12
+ "src/**/*.ts"
13
+ ]
14
+ }