diff --git a/v2/ui/checkbox/checkbox.component.ts b/v2/ui/checkbox/checkbox.component.ts new file mode 100644 index 0000000..402aacb --- /dev/null +++ b/v2/ui/checkbox/checkbox.component.ts @@ -0,0 +1,189 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technologies + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { + booleanAttribute, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + computed, + forwardRef, + inject, + input, + model, + signal, + ViewEncapsulation, +} from '@angular/core'; +import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideCheck, lucideMinus } from '@ng-icons/lucide'; + +import type { ClassValue } from 'clsx'; + +import { ZardIdDirective } from '../directives'; +import { mergeClasses } from '../utils/merge-classes'; + +import { checkboxLabelVariants, checkboxVariants } from './checkbox.variants'; + +type OnTouchedType = () => unknown; +type OnChangeType = (value: boolean) => void; + +@Component({ + standalone: true, + selector: 'z-checkbox, [z-checkbox]', + imports: [NgIcon, ZardIdDirective], + template: ` + + + + @if (iconName(); as icon) { + + + + `, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ZardCheckboxComponent), + multi: true, + }, + ], + viewProviders: [provideIcons({ lucideCheck, lucideMinus })], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'zCheckbox', +}) +export class ZardCheckboxComponent implements ControlValueAccessor { + private readonly cdr = inject(ChangeDetectorRef); + + readonly class = input(''); + readonly disabled = input(false, { transform: booleanAttribute }); + readonly name = input('checkbox'); + readonly zId = input(''); + + // Two-way bindable checked / indeterminate state. + readonly zChecked = model(false); + readonly zIndeterminate = model(false); + + private onChange: OnChangeType = (_value: boolean) => { + // ControlValueAccessor onChange callback + }; + private onTouched: OnTouchedType = () => { + // ControlValueAccessor onTouched callback + }; + + protected readonly classes = computed(() => mergeClasses(checkboxVariants(), this.class())); + protected readonly labelClasses = computed(() => + mergeClasses(checkboxLabelVariants(), this.isDisabled() ? 'opacity-50' : ''), + ); + + // Form-driven disabled state (FormControl.disable()), combined with the input. + private readonly formDisabled = signal(false); + protected readonly isDisabled = computed(() => this.disabled() || this.formDisabled()); + + // Reflect the visual state to assistive technology: an indeterminate + // checkbox is exposed as "mixed" per the WAI-ARIA spec. + protected readonly ariaChecked = computed(() => { + if (this.zIndeterminate()) { + return 'mixed'; + } + return this.zChecked() ? 'true' : 'false'; + }); + + // The overlay icon: a minus for the indeterminate ("mixed") state, a check + // when selected, and nothing when unchecked. + protected readonly iconName = computed<'lucideMinus' | 'lucideCheck' | null>(() => { + if (this.zIndeterminate()) { + return 'lucideMinus'; + } + return this.zChecked() ? 'lucideCheck' : null; + }); + + writeValue(val: unknown): void { + // A concrete form value resolves any prior indeterminate state, otherwise a + // checked value could keep rendering the "mixed" (minus) icon. + this.zIndeterminate.set(false); + this.zChecked.set(!!val); + this.cdr.markForCheck(); + } + + registerOnChange(fn: OnChangeType): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouchedType): void { + this.onTouched = fn; + } + + setDisabledState(disabledState: boolean): void { + // Called by Angular forms (e.g. FormControl.disable()); combined with the + // disabled() input via the isDisabled computed. + this.formDisabled.set(disabledState); + this.cdr.markForCheck(); + } + + onCheckboxBlur(): void { + this.onTouched(); + this.cdr.markForCheck(); + } + + onCheckboxChange(event: Event): void { + if (this.isDisabled()) { + return; + } + + const checked = (event.target as HTMLInputElement | null)?.checked ?? false; + // Any user interaction resolves the indeterminate state. + this.zIndeterminate.set(false); + // Updating the zChecked model automatically emits the zCheckedChange output. + this.zChecked.set(checked); + this.onChange(checked); + this.cdr.markForCheck(); + } +} diff --git a/v2/ui/checkbox/checkbox.variants.ts b/v2/ui/checkbox/checkbox.variants.ts new file mode 100644 index 0000000..d40b956 --- /dev/null +++ b/v2/ui/checkbox/checkbox.variants.ts @@ -0,0 +1,29 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technologies + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { cva } from 'class-variance-authority'; + +export const checkboxVariants = cva( + 'peer absolute inset-0 size-4 appearance-none rounded-[4px] border border-input bg-background shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30 shrink-0 checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary', +); + +export const checkboxLabelVariants = cva('text-sm select-none empty:hidden'); diff --git a/v2/ui/checkbox/index.ts b/v2/ui/checkbox/index.ts new file mode 100644 index 0000000..e015679 --- /dev/null +++ b/v2/ui/checkbox/index.ts @@ -0,0 +1,24 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technologies + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +export * from './checkbox.component'; +export * from './checkbox.variants'; diff --git a/v2/ui/index.ts b/v2/ui/index.ts index 17d0109..2d1a92b 100644 --- a/v2/ui/index.ts +++ b/v2/ui/index.ts @@ -23,6 +23,7 @@ export * from './button'; export * from './calendar'; export * from './card'; +export * from './checkbox'; export * from './date-picker'; export * from './dialog'; export * from './directives';