diff --git a/apps/docs/src/app/structure.ts b/apps/docs/src/app/structure.ts
index ba358dc32f..d4616e4b08 100644
--- a/apps/docs/src/app/structure.ts
+++ b/apps/docs/src/app/structure.ts
@@ -417,7 +417,7 @@ const structure: DocsStructure = makeStructure({
svgPreview: 'checkbox',
hasApi: true,
apiId: 'checkbox',
- hasExamples: false
+ hasExamples: true
},
{
id: DocsStructureItemId.ClampedList,
diff --git a/packages/components-dev/checkbox/module.ts b/packages/components-dev/checkbox/module.ts
index e4b72fde1f..77276d7cca 100644
--- a/packages/components-dev/checkbox/module.ts
+++ b/packages/components-dev/checkbox/module.ts
@@ -18,6 +18,8 @@ import { DevThemeToggle } from '../theme-toggle';
+
+
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
diff --git a/packages/components/checkbox/checkbox-config.ts b/packages/components/checkbox/checkbox-config.ts
index 1c68543bb2..f700095d99 100644
--- a/packages/components/checkbox/checkbox-config.ts
+++ b/packages/components/checkbox/checkbox-config.ts
@@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core';
+import { KbqCheckableClickAction } from '@koobiq/components/core';
/**
* Checkbox click action when user click on input element.
@@ -7,7 +8,7 @@ import { InjectionToken } from '@angular/core';
* check-indeterminate: Toggle checked status, set indeterminate to false. Default behavior.
* undefined: Same as `check-indeterminate`.
*/
-export type KbqCheckboxClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;
+export type KbqCheckboxClickAction = KbqCheckableClickAction;
/**
* Injection token that can be used to specify the checkbox click behavior.
diff --git a/packages/components/checkbox/checkbox.ts b/packages/components/checkbox/checkbox.ts
index 99873cf18f..5a0db6245a 100644
--- a/packages/components/checkbox/checkbox.ts
+++ b/packages/components/checkbox/checkbox.ts
@@ -18,9 +18,17 @@ import {
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { KbqCheckedState, KbqColorDirective } from '@koobiq/components/core';
+import { KbqCheckable, KbqCheckedState, KbqColorDirective, TransitionCheckState } from '@koobiq/components/core';
import { KBQ_CHECKBOX_CLICK_ACTION, KbqCheckboxClickAction } from './checkbox-config';
+/**
+ * Re-exported for backwards compatibility - `TransitionCheckState` moved to `@koobiq/components/core`
+ * so it can be shared with `KbqToggleComponent`. Existing code importing it from here keeps working.
+ * @docs-private
+ * @deprecated Use `TransitionCheckState` from `@koobiq/components/core` instead.
+ */
+export { TransitionCheckState };
+
// Increasing integer for generating unique ids for checkbox components.
let nextUniqueId = 0;
@@ -28,6 +36,7 @@ let nextUniqueId = 0;
* Provider Expression that allows kbq-checkbox to register as a ControlValueAccessor.
* This allows it to support [(ngModel)].
* @docs-private
+ * @deprecated Unused - the `ControlValueAccessor` is now registered by the `KbqCheckable` host directive.
*/
export const KBQ_CHECKBOX_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
@@ -35,21 +44,6 @@ export const KBQ_CHECKBOX_CONTROL_VALUE_ACCESSOR: any = {
multi: true
};
-/**
- * Represents the different states that require custom transitions between them.
- * @docs-private
- */
-export enum TransitionCheckState {
- /** The initial state of the component before any user interaction. */
- Init = 'init',
- /** The state representing the component when it's becoming checked. */
- Checked = 'checked',
- /** The state representing the component when it's becoming unchecked. */
- Unchecked = 'unchecked',
- /** The state representing the component when it's becoming indeterminate. */
- Indeterminate = 'indeterminate'
-}
-
/** Change event object emitted by KbqCheckbox. */
export class KbqCheckboxChange {
/** The source KbqCheckbox of the event. */
@@ -72,7 +66,6 @@ export class KbqCheckboxChange {
],
templateUrl: 'checkbox.html',
styleUrls: ['checkbox.scss', 'checkbox-tokens.scss'],
- providers: [KBQ_CHECKBOX_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
@@ -86,11 +79,13 @@ export class KbqCheckboxChange {
'[class.kbq-disabled]': 'disabled',
'[class.kbq-checkbox_label-before]': 'labelPosition() == "before"'
},
+ hostDirectives: [KbqCheckable],
exportAs: 'kbqCheckbox'
})
export class KbqCheckbox extends KbqColorDirective implements ControlValueAccessor, AfterViewInit, OnDestroy {
- private changeDetectorRef = inject(ChangeDetectorRef);
- private focusMonitor = inject(FocusMonitor);
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly focusMonitor = inject(FocusMonitor);
+ private readonly checkable = inject(KbqCheckable, { self: true });
readonly big = input(false);
@@ -137,48 +132,36 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get checked(): boolean {
- return this._checked;
+ return this.checkable.checked();
}
set checked(value: boolean) {
- if (value !== this.checked) {
- this._checked = value;
- this.changeDetectorRef.markForCheck();
- }
+ this.checkable.checked.set(value);
}
- private _checked: boolean = false;
-
/** Whether the checkbox is disabled. */
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input({ transform: booleanAttribute })
get disabled(): boolean {
- return this._disabled;
+ return this.checkable.disabled();
}
set disabled(value: boolean) {
- if (value !== this.disabled) {
- this._disabled = value;
- this.changeDetectorRef.markForCheck();
- }
+ this.checkable.disabled.set(value);
}
- private _disabled: boolean = false;
-
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input({ transform: numberAttribute })
get tabIndex(): number {
- return this.disabled ? -1 : this._tabIndex;
+ return this.checkable.effectiveTabIndex();
}
set tabIndex(value: number) {
- this._tabIndex = value;
+ this.checkable.tabIndex.set(value);
}
- private _tabIndex = 0;
-
/**
* Whether the checkbox is indeterminate. This is also known as "mixed" mode and can be used to
* represent a checkbox with three states, e.g. a checkbox that represents a nested list of
@@ -189,33 +172,29 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get indeterminate(): boolean {
- return this._indeterminate;
+ return this.checkable.indeterminate();
}
set indeterminate(value: boolean) {
- const changed = value !== this._indeterminate;
+ const changed = value !== this.checkable.indeterminate();
- this._indeterminate = value;
+ this.checkable.indeterminate.set(value);
if (changed) {
- if (this._indeterminate) {
- this.transitionCheckState(TransitionCheckState.Indeterminate);
- } else {
- this.transitionCheckState(this.checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
- }
-
- this.indeterminateChange.emit(this._indeterminate);
+ this.checkable.transitionCheckState(
+ this.checkable.indeterminate()
+ ? TransitionCheckState.Indeterminate
+ : this.checked
+ ? TransitionCheckState.Checked
+ : TransitionCheckState.Unchecked
+ );
+
+ this.indeterminateChange.emit(value);
}
}
- private _indeterminate: boolean = false;
-
private uniqueId: string = `kbq-checkbox-${++nextUniqueId}`;
- private currentAnimationClass: string = '';
-
- private currentCheckState: TransitionCheckState = TransitionCheckState.Init;
-
constructor() {
super();
@@ -225,6 +204,8 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
/**
* Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor.
* @docs-private
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
*/
onTouched: () => any = () => {};
@@ -246,33 +227,49 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
this.changeDetectorRef.markForCheck();
}
- // Implemented as part of ControlValueAccessor.
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
writeValue(value: any) {
this.checked = !!value;
}
- // Implemented as part of ControlValueAccessor.
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
registerOnChange(fn: (value: any) => void) {
- this.controlValueAccessorChangeFn = fn;
+ this.checkable.registerOnChange(fn);
}
- // Implemented as part of ControlValueAccessor.
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
registerOnTouched(fn: any) {
- this.onTouched = fn;
+ this.checkable.registerOnTouched(fn);
}
- // Implemented as part of ControlValueAccessor.
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
getAriaChecked(): KbqCheckedState {
- return this.checked ? 'true' : this.indeterminate ? 'mixed' : 'false';
+ return this.checkable.getAriaChecked();
}
/** Toggles the `checked` state of the checkbox. */
toggle(): void {
- this.checked = !this.checked;
+ this.checkable.toggle();
}
/**
@@ -292,30 +289,30 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
// Preventing bubbling for the second event will solve that issue.
event.stopPropagation();
- // If resetIndeterminate is false, and the current state is indeterminate, do nothing on click
- if (!this.disabled && this.clickAction !== 'noop') {
+ const { shouldToggle, shouldClearIndeterminate } = this.checkable.resolveClick(this.clickAction);
+
+ if (shouldToggle) {
// When user manually click on the checkbox, `indeterminate` is set to false.
- if (this.indeterminate && this.clickAction !== 'check') {
+ if (shouldClearIndeterminate) {
Promise.resolve().then(() => {
- this._indeterminate = false;
- this.indeterminateChange.emit(this._indeterminate);
+ this.checkable.indeterminate.set(false);
+ this.indeterminateChange.emit(false);
});
}
this.toggle();
- this.transitionCheckState(this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
+ this.checkable.transitionCheckState(
+ this.checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked
+ );
// Emit our custom change event if the native input emitted one.
// It is important to only emit it, if the native input triggered one, because
// we don't want to trigger a change event, when the `checked` variable changes for example.
this.emitChangeEvent();
- } else if (!this.disabled && this.clickAction === 'noop') {
+ } else if (!this.disabled) {
// Reset native input when clicked with noop. The native checkbox becomes checked after
// click, reset it to be align with `checked` value of `kbq-checkbox`.
- const inputElement = this.inputElement();
-
- inputElement.nativeElement.checked = this.checked;
- inputElement.nativeElement.indeterminate = this.indeterminate;
+ this.checkable.resetNativeInput(this.inputElement().nativeElement);
}
}
@@ -330,41 +327,21 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
// emit its event object to the `change` output.
event.stopPropagation();
}
- private controlValueAccessorChangeFn: (value: any) => void = () => {};
-
- private transitionCheckState(newState: TransitionCheckState) {
- const oldState = this.currentCheckState;
- const element: HTMLElement = this.elementRef.nativeElement;
-
- if (oldState === newState) {
- return;
- }
-
- if (this.currentAnimationClass.length > 0) {
- element.classList.remove(this.currentAnimationClass);
- }
-
- this.currentCheckState = newState;
-
- if (this.currentAnimationClass.length > 0) {
- element.classList.add(this.currentAnimationClass);
- }
- }
private emitChangeEvent() {
+ // Note: `toggle()` already notifies the ControlValueAccessor change handler via `KbqCheckable.toggle()`.
const event = new KbqCheckboxChange();
event.source = this;
event.checked = this.checked;
- this.controlValueAccessorChangeFn(this.checked);
this.change.emit(event);
}
/** Function is called whenever the focus changes for the input element. */
private onInputFocusChange(focusOrigin: FocusOrigin) {
if (focusOrigin) {
- this.onTouched();
+ this.checkable.onTouched();
}
}
}
diff --git a/packages/components/checkbox/examples.checkbox.en.md b/packages/components/checkbox/examples.checkbox.en.md
index 2faef3848c..58ab0a405f 100644
--- a/packages/components/checkbox/examples.checkbox.en.md
+++ b/packages/components/checkbox/examples.checkbox.en.md
@@ -1,5 +1,5 @@
-🚧 **Documentation in progress** 🚧
+### Block checkbox
-Unfortunately, the documentation for this section is not ready yet. We are actively working on its creation and plan to add it soon.
+A custom checkbox-like "card" control built on the shared `KbqCheckable` primitive (`@koobiq/components/core`), showing how to reuse checkbox's click/form/a11y behavior with your own markup and styling.
-If you would like to contribute to the documentation or have any questions, please feel free to [open an issue](https://github.com/koobiq/angular-components/issues) in our GitHub repository.
+
diff --git a/packages/components/checkbox/examples.checkbox.ru.md b/packages/components/checkbox/examples.checkbox.ru.md
index b7a203c6ab..06ee622f86 100644
--- a/packages/components/checkbox/examples.checkbox.ru.md
+++ b/packages/components/checkbox/examples.checkbox.ru.md
@@ -1,5 +1,5 @@
-🚧 **Документация в процессе написания** 🚧
+### Блочный чекбокс
-К сожалению, документация для этого раздела еще не готова. Мы активно работаем над ее созданием и планируем добавить в ближайшее время.
+Кастомный чекбокс-подобный элемент в виде карточки, построенный на общем примитиве `KbqCheckable` (`@koobiq/components/core`) — демонстрирует повторное использование логики клика, форм и доступности с собственной разметкой и стилями.
-Если вы хотите помочь в написании документации или у вас есть вопросы, пожалуйста, [создайте issue](https://github.com/koobiq/angular-components/issues) в нашем репозитории на GitHub.
+
diff --git a/packages/components/core/common-behaviors/checkable.spec.ts b/packages/components/core/common-behaviors/checkable.spec.ts
new file mode 100644
index 0000000000..dd49d5062f
--- /dev/null
+++ b/packages/components/core/common-behaviors/checkable.spec.ts
@@ -0,0 +1,334 @@
+import { Component, ElementRef, inject, Provider, Type, viewChild } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { KbqCheckable, KbqCheckableClickAction, TransitionCheckState } from './checkable';
+
+const createComponent = (component: Type, providers: Provider[] = []): ComponentFixture => {
+ TestBed.configureTestingModule({
+ imports: [component, FormsModule],
+ providers
+ });
+
+ const fixture = TestBed.createComponent(component);
+
+ fixture.autoDetectChanges();
+
+ return fixture;
+};
+
+@Component({
+ selector: 'test-checkable',
+ template: `
+
+ `,
+ hostDirectives: [
+ { directive: KbqCheckable, inputs: ['checked', 'disabled', 'indeterminate', 'tabIndex'] }
+ ]
+})
+class TestCheckable {
+ protected readonly checkable = inject(KbqCheckable, { self: true });
+
+ private readonly input = viewChild.required>('input');
+
+ clickAction: KbqCheckableClickAction;
+
+ onInputClick(event: Event): void {
+ event.stopPropagation();
+
+ const { shouldToggle, shouldClearIndeterminate } = this.checkable.resolveClick(this.clickAction);
+
+ if (shouldToggle) {
+ if (shouldClearIndeterminate) {
+ this.checkable.indeterminate.set(false);
+ }
+
+ this.checkable.toggle();
+ } else {
+ this.checkable.resetNativeInput(this.input().nativeElement);
+ }
+ }
+}
+
+@Component({
+ imports: [TestCheckable, FormsModule],
+ template: `
+
+ `
+})
+class TestCheckableWithNgModel {
+ checked = false;
+}
+
+describe(KbqCheckable.name, () => {
+ let fixture: ComponentFixture;
+ let hostInstance: TestCheckable;
+ let checkable: KbqCheckable;
+ let inputElement: HTMLInputElement;
+
+ beforeEach(() => {
+ fixture = createComponent(TestCheckable);
+ hostInstance = fixture.componentInstance;
+ checkable = fixture.debugElement.injector.get(KbqCheckable);
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
+ });
+
+ describe('state', () => {
+ it('should default to unchecked, enabled, determinate, tabIndex 0', () => {
+ expect(checkable.checked()).toBe(false);
+ expect(checkable.disabled()).toBe(false);
+ expect(checkable.indeterminate()).toBe(false);
+ expect(checkable.tabIndex()).toBe(0);
+ expect(checkable.effectiveTabIndex()).toBe(0);
+ });
+
+ it('should force effectiveTabIndex to -1 while disabled, without changing the raw tabIndex', () => {
+ checkable.tabIndex.set(3);
+ checkable.disabled.set(true);
+ fixture.detectChanges();
+
+ expect(checkable.effectiveTabIndex()).toBe(-1);
+ expect(checkable.tabIndex()).toBe(3);
+
+ checkable.disabled.set(false);
+ fixture.detectChanges();
+
+ expect(checkable.effectiveTabIndex()).toBe(3);
+ });
+
+ it('should reflect state changes on the bound native input', () => {
+ checkable.checked.set(true);
+ checkable.disabled.set(true);
+ checkable.indeterminate.set(true);
+ fixture.detectChanges();
+
+ expect(inputElement.checked).toBe(true);
+ expect(inputElement.disabled).toBe(true);
+ expect(inputElement.indeterminate).toBe(true);
+ });
+ });
+
+ describe('toggle', () => {
+ it('should flip checked', () => {
+ expect(checkable.checked()).toBe(false);
+
+ checkable.toggle();
+ expect(checkable.checked()).toBe(true);
+
+ checkable.toggle();
+ expect(checkable.checked()).toBe(false);
+ });
+
+ it('should notify the registered ControlValueAccessor change handler with the new value', () => {
+ const onChange = jest.fn();
+
+ checkable.registerOnChange(onChange);
+
+ checkable.toggle();
+
+ expect(onChange).toHaveBeenCalledWith(true);
+
+ checkable.toggle();
+
+ expect(onChange).toHaveBeenCalledWith(false);
+ expect(onChange).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('getAriaChecked', () => {
+ it('should return "false" by default', () => {
+ expect(checkable.getAriaChecked()).toBe('false');
+ });
+
+ it('should return "true" when checked', () => {
+ checkable.checked.set(true);
+ expect(checkable.getAriaChecked()).toBe('true');
+ });
+
+ it('should return "mixed" when indeterminate and not checked', () => {
+ checkable.indeterminate.set(true);
+ expect(checkable.getAriaChecked()).toBe('mixed');
+ });
+
+ it('should prefer "true" over "mixed" when both checked and indeterminate', () => {
+ checkable.checked.set(true);
+ checkable.indeterminate.set(true);
+ expect(checkable.getAriaChecked()).toBe('true');
+ });
+ });
+
+ describe('resolveClick', () => {
+ it('should not toggle while disabled, regardless of clickAction', () => {
+ checkable.disabled.set(true);
+
+ expect(checkable.resolveClick(undefined)).toEqual({ shouldToggle: false, shouldClearIndeterminate: false });
+ expect(checkable.resolveClick('check-indeterminate')).toEqual({
+ shouldToggle: false,
+ shouldClearIndeterminate: false
+ });
+ });
+
+ it('should not toggle when clickAction is "noop"', () => {
+ expect(checkable.resolveClick('noop')).toEqual({ shouldToggle: false, shouldClearIndeterminate: false });
+ });
+
+ it('should toggle and clear indeterminate by default (undefined/check-indeterminate)', () => {
+ checkable.indeterminate.set(true);
+
+ expect(checkable.resolveClick(undefined)).toEqual({ shouldToggle: true, shouldClearIndeterminate: true });
+ expect(checkable.resolveClick('check-indeterminate')).toEqual({
+ shouldToggle: true,
+ shouldClearIndeterminate: true
+ });
+ });
+
+ it('should toggle without clearing indeterminate when clickAction is "check"', () => {
+ checkable.indeterminate.set(true);
+
+ expect(checkable.resolveClick('check')).toEqual({ shouldToggle: true, shouldClearIndeterminate: false });
+ });
+
+ it('should not report shouldClearIndeterminate when not indeterminate', () => {
+ expect(checkable.resolveClick(undefined)).toEqual({ shouldToggle: true, shouldClearIndeterminate: false });
+ });
+ });
+
+ describe('resetNativeInput', () => {
+ it('should sync the native input to the current checked/indeterminate state', () => {
+ checkable.checked.set(true);
+ checkable.indeterminate.set(true);
+
+ inputElement.checked = false;
+ inputElement.indeterminate = false;
+
+ checkable.resetNativeInput(inputElement);
+
+ expect(inputElement.checked).toBe(true);
+ expect(inputElement.indeterminate).toBe(true);
+ });
+ });
+
+ describe('transitionCheckState', () => {
+ it('should update currentCheckState', () => {
+ expect(checkable.currentCheckState()).toBe(TransitionCheckState.Init);
+
+ checkable.transitionCheckState(TransitionCheckState.Checked);
+
+ expect(checkable.currentCheckState()).toBe(TransitionCheckState.Checked);
+ });
+
+ it('should be a no-op when transitioning to the same state', () => {
+ checkable.transitionCheckState(TransitionCheckState.Checked);
+
+ const setSpy = jest.spyOn(checkable.currentCheckState, 'set');
+
+ checkable.transitionCheckState(TransitionCheckState.Checked);
+
+ expect(setSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('click handling (via host)', () => {
+ it('should toggle checked on click', () => {
+ expect(checkable.checked()).toBe(false);
+
+ inputElement.click();
+ fixture.detectChanges();
+
+ expect(checkable.checked()).toBe(true);
+ });
+
+ it('should not toggle when disabled', () => {
+ checkable.disabled.set(true);
+ fixture.detectChanges();
+
+ inputElement.click();
+ fixture.detectChanges();
+
+ expect(checkable.checked()).toBe(false);
+ });
+
+ it('should clear indeterminate on click by default', () => {
+ checkable.indeterminate.set(true);
+ fixture.detectChanges();
+
+ inputElement.click();
+ fixture.detectChanges();
+
+ expect(checkable.indeterminate()).toBe(false);
+ expect(checkable.checked()).toBe(true);
+ });
+
+ it('should not toggle on click when clickAction is "noop"', () => {
+ hostInstance.clickAction = 'noop';
+
+ inputElement.click();
+ fixture.detectChanges();
+
+ expect(checkable.checked()).toBe(false);
+ });
+ });
+
+ describe('ControlValueAccessor', () => {
+ it('writeValue should coerce the value and set checked', () => {
+ checkable.writeValue(1);
+ expect(checkable.checked()).toBe(true);
+
+ checkable.writeValue(null);
+ expect(checkable.checked()).toBe(false);
+ });
+
+ it('registerOnChange should wire up notifyFormValueChange', () => {
+ const onChange = jest.fn();
+
+ checkable.registerOnChange(onChange);
+ checkable.notifyFormValueChange(true);
+
+ expect(onChange).toHaveBeenCalledWith(true);
+ });
+
+ it('registerOnTouched should wire up onTouched', () => {
+ const onTouched = jest.fn();
+
+ checkable.registerOnTouched(onTouched);
+ checkable.onTouched();
+
+ expect(onTouched).toHaveBeenCalled();
+ });
+
+ it('setDisabledState should set disabled', () => {
+ checkable.setDisabledState(true);
+ expect(checkable.disabled()).toBe(true);
+ });
+ });
+});
+
+describe(`${KbqCheckable.name} integration with ngModel`, () => {
+ let ngModelFixture: ComponentFixture;
+
+ beforeEach(() => {
+ ngModelFixture = createComponent(TestCheckableWithNgModel);
+ });
+
+ it('should support two-way binding through the KbqCheckable ControlValueAccessor', fakeAsync(() => {
+ const testInput = ngModelFixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
+
+ tick();
+
+ expect(ngModelFixture.componentInstance.checked).toBe(false);
+
+ testInput.click();
+ ngModelFixture.detectChanges();
+ tick();
+
+ expect(ngModelFixture.componentInstance.checked).toBe(true);
+ }));
+});
diff --git a/packages/components/core/common-behaviors/checkable.ts b/packages/components/core/common-behaviors/checkable.ts
new file mode 100644
index 0000000000..56d5192a6d
--- /dev/null
+++ b/packages/components/core/common-behaviors/checkable.ts
@@ -0,0 +1,153 @@
+import { computed, Directive, forwardRef, InjectionToken, model, Provider, signal } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { KbqCheckedState } from './checkbox';
+
+/**
+ * Provider Expression that allows `KbqCheckable` to register as a `ControlValueAccessor`, so any host
+ * applying it via `hostDirectives` gets `[(ngModel)]`/`formControl` support without wiring up its own.
+ * @docs-private
+ */
+export const KBQ_CHECKABLE_CONTROL_VALUE_ACCESSOR: Provider = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => KbqCheckable),
+ multi: true
+};
+
+/**
+ * Click action shared by checkable controls (checkbox, toggle, and custom checkbox-like primitives).
+ * noop: Do not toggle checked or indeterminate.
+ * check: Only toggle checked status, ignore indeterminate.
+ * check-indeterminate: Toggle checked status, set indeterminate to false. Default behavior.
+ * undefined: Same as `check-indeterminate`.
+ */
+export type KbqCheckableClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;
+
+/** Injection token that can be used to specify the default click behavior for checkable controls. */
+export const KBQ_CHECKABLE_CLICK_ACTION = new InjectionToken('kbq-checkable-click-action');
+
+/**
+ * Represents the different states that require custom transitions between them.
+ * @docs-private
+ */
+export enum TransitionCheckState {
+ /** The initial state of the component before any user interaction. */
+ Init = 'init',
+ /** The state representing the component when it's becoming checked. */
+ Checked = 'checked',
+ /** The state representing the component when it's becoming unchecked. */
+ Unchecked = 'unchecked',
+ /** The state representing the component when it's becoming indeterminate. */
+ Indeterminate = 'indeterminate'
+}
+
+/** Result of resolving a click on a checkable control's native input. See {@link KbqCheckable.resolveClick}. */
+export interface KbqCheckableClickResult {
+ /** Whether `checked` should be toggled and a `change` event emitted. */
+ readonly shouldToggle: boolean;
+ /** Whether `indeterminate` should be cleared and an `indeterminateChange` event emitted. */
+ readonly shouldClearIndeterminate: boolean;
+}
+
+/**
+ * Shared behavior for checkbox-like controls: checked/disabled/indeterminate/tabIndex state, the click-to-toggle
+ * algorithm, ARIA-checked computation, and `ControlValueAccessor` change plumbing.
+ *
+ * Meant to be applied via `hostDirectives` by components that render their own native ``
+ * and markup (e.g. `KbqCheckbox`, `KbqToggleComponent`, or custom checkbox-like primitives). Hosts stay
+ * responsible for their own public inputs/outputs, `FocusMonitor` wiring, and emitting their own typed
+ * `change` event - this directive only centralizes the state and decisions behind them.
+ */
+@Directive({
+ selector: '[kbqCheckable]',
+ providers: [KBQ_CHECKABLE_CONTROL_VALUE_ACCESSOR],
+ exportAs: 'kbqCheckable'
+})
+export class KbqCheckable implements ControlValueAccessor {
+ private controlValueAccessorChangeFn: (value: any) => void = () => {};
+
+ /**
+ * Called when the control is blurred. Needed to properly implement `ControlValueAccessor`.
+ * @docs-private
+ */
+ onTouched: () => any = () => {};
+
+ /** @docs-private */
+ readonly currentCheckState = signal(TransitionCheckState.Init);
+
+ readonly checked = model(false);
+
+ readonly disabled = model(false);
+
+ readonly indeterminate = model(false);
+
+ /** Raw tab index as set by the caller, ignoring `disabled`. Use `effectiveTabIndex` to read the applied value. */
+ readonly tabIndex = model(0);
+
+ /** The tab index actually applied to the native input: forced to `-1` while disabled. */
+ readonly effectiveTabIndex = computed(() => (this.disabled() ? -1 : this.tabIndex()));
+
+ /** Returns the ARIA-checked value: `'true'`, `'false'`, or `'mixed'` when indeterminate. */
+ getAriaChecked(): KbqCheckedState {
+ return this.checked() ? 'true' : this.indeterminate() ? 'mixed' : 'false';
+ }
+
+ /** Toggles the `checked` state and notifies the registered `ControlValueAccessor` change handler. */
+ toggle(): void {
+ this.checked.update((value) => !value);
+ this.notifyFormValueChange(this.checked());
+ }
+
+ /**
+ * Resolves what a click on the native input should do, based on the current state and `clickAction`.
+ * Pure - does not mutate state. Callers apply `checked`/`indeterminate` themselves and own emitting
+ * their own change events, so timing/order stays identical to a hand-written click handler.
+ */
+ resolveClick(clickAction: KbqCheckableClickAction): KbqCheckableClickResult {
+ if (this.disabled() || clickAction === 'noop') {
+ return { shouldToggle: false, shouldClearIndeterminate: false };
+ }
+
+ return {
+ shouldToggle: true,
+ shouldClearIndeterminate: this.indeterminate() && clickAction !== 'check'
+ };
+ }
+
+ /** Resets the native input to match the current `checked`/`indeterminate` state (used for the `noop` click action). */
+ resetNativeInput(nativeInput: HTMLInputElement): void {
+ nativeInput.checked = this.checked();
+ nativeInput.indeterminate = this.indeterminate();
+ }
+
+ /** Transitions the animation state, ignoring no-op transitions. @docs-private */
+ transitionCheckState(newState: TransitionCheckState): void {
+ if (this.currentCheckState() === newState) return;
+
+ this.currentCheckState.set(newState);
+ }
+
+ // Implemented as part of ControlValueAccessor.
+ writeValue(value: any): void {
+ this.checked.set(!!value);
+ }
+
+ /** Stores the `ControlValueAccessor` change handler to be notified by `notifyFormValueChange`. */
+ registerOnChange(fn: (value: any) => void): void {
+ this.controlValueAccessorChangeFn = fn;
+ }
+
+ // Implemented as part of ControlValueAccessor.
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ // Implemented as part of ControlValueAccessor.
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled.set(isDisabled);
+ }
+
+ /** Notifies the registered `ControlValueAccessor` change handler. */
+ notifyFormValueChange(value: boolean): void {
+ this.controlValueAccessorChangeFn(value);
+ }
+}
diff --git a/packages/components/core/common-behaviors/index.ts b/packages/components/core/common-behaviors/index.ts
index 57c182ee98..46e7052216 100644
--- a/packages/components/core/common-behaviors/index.ts
+++ b/packages/components/core/common-behaviors/index.ts
@@ -1,5 +1,6 @@
import { InjectionToken } from '@angular/core';
+export * from './checkable';
export * from './checkbox';
export * from './clipboard';
export { CanColor, KbqColorDirective, KbqComponentColors, ThemePalette } from './color';
diff --git a/packages/components/toggle/toggle.component.ts b/packages/components/toggle/toggle.component.ts
index 06da8aae18..cda057f19b 100644
--- a/packages/components/toggle/toggle.component.ts
+++ b/packages/components/toggle/toggle.component.ts
@@ -7,8 +7,8 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ effect,
ElementRef,
- forwardRef,
inject,
Input,
input,
@@ -18,9 +18,18 @@ import {
viewChild,
ViewEncapsulation
} from '@angular/core';
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { KBQ_CHECKBOX_CLICK_ACTION, KbqCheckboxClickAction, TransitionCheckState } from '@koobiq/components/checkbox';
-import { KbqAnimationCurves, KbqAnimationDurations, KbqCheckedState, KbqColorDirective } from '@koobiq/components/core';
+import { ControlValueAccessor } from '@angular/forms';
+import { KBQ_CHECKBOX_CLICK_ACTION } from '@koobiq/components/checkbox';
+import {
+ KBQ_CHECKABLE_CLICK_ACTION,
+ KbqAnimationCurves,
+ KbqAnimationDurations,
+ KbqCheckable,
+ KbqCheckableClickAction,
+ KbqCheckedState,
+ KbqColorDirective,
+ TransitionCheckState
+} from '@koobiq/components/core';
let nextUniqueId = 0;
@@ -31,6 +40,11 @@ export class KbqToggleChange {
checked: boolean;
}
+/**
+ * Toggle click action when user click on input element. Alias of `KbqCheckableClickAction`.
+ */
+export type KbqToggleClickAction = KbqCheckableClickAction;
+
@Component({
selector: 'kbq-toggle',
imports: [
@@ -39,11 +53,9 @@ export class KbqToggleChange {
templateUrl: './toggle.component.html',
styleUrls: ['./toggle.scss', './toggle-tokens.scss'],
providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => KbqToggleComponent),
- multi: true
- }
+ // Falls back to `KBQ_CHECKBOX_CLICK_ACTION` for backwards compatibility with apps that already
+ // configure it globally to control click behavior for both checkbox and toggle.
+ { provide: KBQ_CHECKABLE_CLICK_ACTION, useFactory: () => inject(KBQ_CHECKBOX_CLICK_ACTION, { optional: true }) }
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
@@ -56,6 +68,7 @@ export class KbqToggleChange {
'[class.kbq-active]': 'checked',
'[class.kbq-indeterminate]': 'indeterminate'
},
+ hostDirectives: [KbqCheckable],
animations: [
trigger('switch', [
state(TransitionCheckState.Init, style({ left: '3px' })),
@@ -79,8 +92,9 @@ export class KbqToggleChange {
exportAs: 'kbqToggle'
})
export class KbqToggleComponent extends KbqColorDirective implements AfterViewInit, ControlValueAccessor, OnDestroy {
- private focusMonitor = inject(FocusMonitor);
- private changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly focusMonitor = inject(FocusMonitor);
+ private readonly checkable = inject(KbqCheckable, { self: true });
readonly big = input(false);
@@ -107,48 +121,38 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get disabled() {
- return this._disabled;
+ return this.checkable.disabled();
}
set disabled(value: any) {
- if (value !== this._disabled) {
- this._disabled = value;
- this.changeDetectorRef.markForCheck();
- }
+ this.checkable.disabled.set(value);
}
- private _disabled: boolean = false;
-
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input({ transform: numberAttribute })
get tabIndex(): number {
- return this.disabled ? -1 : this._tabIndex;
+ return this.checkable.effectiveTabIndex();
}
set tabIndex(value: number) {
- this._tabIndex = value;
+ this.checkable.tabIndex.set(value);
}
- private _tabIndex = 0;
-
get checked() {
- return this._checked;
+ return this.checkable.checked();
}
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
set checked(value: boolean) {
- if (value !== this._checked) {
- this._checked = value;
+ if (value !== this.checkable.checked()) {
+ this.checkable.checked.set(value);
this.setTransitionCheckState();
- this.changeDetectorRef.markForCheck();
}
}
- private _checked: boolean = false;
-
/**
* Whether the toggle is indeterminate. This is also known as "mixed" mode and can be used to
* represent a checkbox with three states, e.g. a checkbox that represents a nested list of
@@ -159,21 +163,20 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
// Accessor inputs cannot be migrated as they are too complex.
@Input({ transform: booleanAttribute })
get indeterminate(): boolean {
- return this._indeterminate;
+ return this.checkable.indeterminate();
}
set indeterminate(value: boolean) {
- const changed = value !== this._indeterminate;
+ const changed = value !== this.checkable.indeterminate();
- this._indeterminate = value;
+ this.checkable.indeterminate.set(value);
if (changed) {
this.setTransitionCheckState();
- this.indeterminateChange.emit(this._indeterminate);
+ this.indeterminateChange.emit(value);
}
}
- private _indeterminate: boolean = false;
/**
* Property for manually set loading state.
*/
@@ -190,7 +193,7 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
/** Defines the behavior when a user clicks on the toggle. */
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
- @Input() clickAction: KbqCheckboxClickAction = inject(KBQ_CHECKBOX_CLICK_ACTION, { optional: true }) || undefined;
+ @Input() clickAction: KbqToggleClickAction = inject(KBQ_CHECKABLE_CLICK_ACTION, { optional: true }) || undefined;
private uniqueId: string = `kbq-toggle-${++nextUniqueId}`;
@@ -198,6 +201,10 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
super();
this.id = this.uniqueId;
+
+ // `writeValue` (ngModel/formControl) now runs on `KbqCheckable`, bypassing the `checked`/`indeterminate`
+ // setters below, so this keeps the `[@switch]` animation state in sync for form-driven value changes too.
+ effect(() => this.setTransitionCheckState());
}
ngAfterViewInit(): void {
@@ -213,7 +220,7 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
}
getAriaChecked(): KbqCheckedState {
- return this.checked ? 'true' : this.indeterminate ? 'mixed' : 'false';
+ return this.checkable.getAriaChecked();
}
onChangeEvent(event: Event) {
@@ -235,44 +242,63 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
// Preventing bubbling for the second event will solve that issue.
event.stopPropagation();
- if (!this.disabled && this.clickAction !== 'noop') {
+ const { shouldToggle, shouldClearIndeterminate } = this.checkable.resolveClick(this.clickAction);
+
+ if (shouldToggle) {
// When user manually click on the toggle, `indeterminate` is set to false.
- if (this.indeterminate && this.clickAction !== 'check') {
+ if (shouldClearIndeterminate) {
Promise.resolve().then(() => {
- this._indeterminate = false;
- this.indeterminateChange.emit(this._indeterminate);
+ this.checkable.indeterminate.set(false);
+ this.indeterminateChange.emit(false);
});
}
- this._checked = !this.checked;
- this.onTouchedCallback();
- this.transitionCheckState(this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
+ this.checkable.toggle();
+ this.checkable.onTouched();
+ this.transitionCheckState(this.checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
// Emit our custom change event if the native input emitted one.
// It is important to only emit it, if the native input triggered one, because
// we don't want to trigger a change event, when the `checked` variable changes for example.
this.emitChangeEvent();
- } else if (!this.disabled && this.clickAction === 'noop') {
+ } else if (!this.disabled) {
// Reset native input when clicked with noop. The native checkbox becomes checked after
// click, reset it to be align with `checked` value of `kbq-toggle`.
- const inputElement = this.inputElement();
-
- inputElement.nativeElement.checked = this.checked;
- inputElement.nativeElement.indeterminate = this.indeterminate;
+ this.checkable.resetNativeInput(this.inputElement().nativeElement);
}
}
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
writeValue(value: any) {
this.checked = !!value;
}
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
registerOnChange(fn: any) {
- this.onChangeCallback = fn;
+ this.checkable.registerOnChange(fn);
}
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
registerOnTouched(fn: any) {
- this.onTouchedCallback = fn;
+ this.checkable.registerOnTouched(fn);
}
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * @deprecated Unused - `ControlValueAccessor` is now implemented by the `KbqCheckable` host directive,
+ * so this is never called by Angular forms. Will be removed in the next major version.
+ */
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
@@ -285,25 +311,19 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
}
}
- private onTouchedCallback = () => {};
-
- private onChangeCallback = (_: any) => {};
-
private transitionCheckState(newState: TransitionCheckState) {
- const oldState = this.currentCheckState;
-
- if (oldState === newState) return;
+ if (this.currentCheckState === newState) return;
this.currentCheckState = newState;
}
private emitChangeEvent() {
+ // Note: `toggle()` already notifies the ControlValueAccessor change handler via `KbqCheckable.toggle()`.
const event = new KbqToggleChange();
event.source = this;
event.checked = this.checked;
- this.onChangeCallback(this.checked);
this.change.emit(event);
}
}
diff --git a/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.css b/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.css
new file mode 100644
index 0000000000..3809e64267
--- /dev/null
+++ b/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.css
@@ -0,0 +1,66 @@
+.example-block-checkbox__layout {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: var(--kbq-size-m);
+ box-sizing: border-box;
+ width: 260px;
+ padding: var(--kbq-size-l);
+ border: 1px solid var(--kbq-line-contrast-fade);
+ border-radius: var(--kbq-size-s);
+ background: var(--kbq-background-card);
+ cursor: pointer;
+ transition: border-color 150ms ease-in-out;
+}
+
+.example-block-checkbox__input {
+ inset: 0;
+ position: absolute;
+
+ margin: 0;
+ opacity: 0;
+ cursor: inherit;
+}
+
+:host(.example-block-checkbox_checked) .example-block-checkbox__layout {
+ border-color: var(--kbq-line-theme);
+}
+
+:host(.example-block-checkbox_disabled) .example-block-checkbox__layout {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.example-block-checkbox__thumb {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--kbq-size-xxs);
+ background: var(--kbq-background-bg-secondary);
+}
+
+.example-block-checkbox__description {
+ color: var(--kbq-foreground-contrast-secondary);
+}
+
+.example-block-checkbox__indicator {
+ position: absolute;
+ top: var(--kbq-size-l);
+ right: var(--kbq-size-l);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 20px;
+ height: 20px;
+ border: 1px solid var(--kbq-line-contrast-fade);
+ border-radius: 50%;
+}
+
+:host(.example-block-checkbox_checked) .example-block-checkbox__indicator {
+ border-color: var(--kbq-line-theme);
+ background: var(--kbq-line-theme);
+}
+
+:host(.example-block-checkbox_checked) .example-block-checkbox__indicator .kbq-icon {
+ color: var(--kbq-foreground-white);
+}
diff --git a/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.ts b/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.ts
new file mode 100644
index 0000000000..1237171db0
--- /dev/null
+++ b/packages/docs-examples/components/checkbox/block-checkbox/block-checkbox-example.ts
@@ -0,0 +1,100 @@
+import { ChangeDetectionStrategy, Component, inject, model, output } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { KbqCheckable } from '@koobiq/components/core';
+import { KbqIconModule } from '@koobiq/components/icon';
+
+/**
+ * EXAMPLE ONLY - not a published Koobiq component.
+ *
+ * A checkbox-like "card" control built directly on top of the shared `KbqCheckable` primitive.
+ * Demonstrates that custom markup/styling can reuse the same click, form, and a11y behavior as
+ * `kbq-checkbox` and `kbq-toggle` via `hostDirectives`, without reimplementing any of it.
+ */
+@Component({
+ selector: 'block-checkbox-component',
+ imports: [KbqIconModule],
+ template: `
+
+ `,
+ styleUrl: './block-checkbox-example.css',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'example-block-checkbox',
+ '[class.example-block-checkbox_checked]': 'checkable.checked()',
+ '[class.example-block-checkbox_disabled]': 'checkable.disabled()'
+ },
+ hostDirectives: [
+ { directive: KbqCheckable, inputs: ['checked', 'disabled'] }
+ ]
+})
+export class BlockCheckboxComponent {
+ protected readonly checkable = inject(KbqCheckable, { self: true });
+
+ /** Emitted with the new `checked` value when the card is toggled by the user. */
+ readonly checkedChange = output();
+
+ protected onInputClick(event: Event): void {
+ // See KbqCheckbox/KbqToggleComponent#onInputClick - stops the label's generated click event
+ // on the native input from bubbling and firing this handler a second time.
+ event.stopPropagation();
+
+ const { shouldToggle } = this.checkable.resolveClick(undefined);
+
+ if (shouldToggle) {
+ this.checkable.toggle();
+ this.checkedChange.emit(this.checkable.checked());
+ }
+ }
+}
+
+/**
+ * @title Custom checkbox block
+ */
+@Component({
+ selector: 'block-checkbox-example',
+ imports: [KbqIconModule, BlockCheckboxComponent, FormsModule],
+ template: `
+
+ Title
+ Lorem ipsum
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'layout-margin-5xl layout-align-center-center layout-row'
+ }
+})
+export class BlockCheckboxExample {
+ checked = model(true);
+}
diff --git a/packages/docs-examples/components/checkbox/index.ts b/packages/docs-examples/components/checkbox/index.ts
index 32793fadbc..23890fdcd7 100644
--- a/packages/docs-examples/components/checkbox/index.ts
+++ b/packages/docs-examples/components/checkbox/index.ts
@@ -1,16 +1,24 @@
import { NgModule } from '@angular/core';
+import { BlockCheckboxExample } from './block-checkbox/block-checkbox-example';
import { CheckboxIndeterminateExample } from './checkbox-indeterminate/checkbox-indeterminate-example';
import { CheckboxMultilineExample } from './checkbox-multiline/checkbox-multiline-example';
import { CheckboxOverviewExample } from './checkbox-overview/checkbox-overview-example';
import { PseudoCheckboxExample } from './pseudo-checkbox/pseudo-checkbox-example';
-export { CheckboxIndeterminateExample, CheckboxMultilineExample, CheckboxOverviewExample, PseudoCheckboxExample };
+export {
+ BlockCheckboxExample,
+ CheckboxIndeterminateExample,
+ CheckboxMultilineExample,
+ CheckboxOverviewExample,
+ PseudoCheckboxExample
+};
const EXAMPLES = [
CheckboxIndeterminateExample,
CheckboxOverviewExample,
PseudoCheckboxExample,
- CheckboxMultilineExample
+ CheckboxMultilineExample,
+ BlockCheckboxExample
];
@NgModule({
diff --git a/tools/public_api_guard/components/checkbox.api.md b/tools/public_api_guard/components/checkbox.api.md
index 3af8abd98f..c0cf6ae09f 100644
--- a/tools/public_api_guard/components/checkbox.api.md
+++ b/tools/public_api_guard/components/checkbox.api.md
@@ -9,16 +9,19 @@ import * as _angular_core from '@angular/core';
import { CheckboxRequiredValidator } from '@angular/forms';
import { ControlValueAccessor } from '@angular/forms';
import { ElementRef } from '@angular/core';
+import * as i1 from '@koobiq/components/core';
import { InjectionToken } from '@angular/core';
+import { KbqCheckableClickAction } from '@koobiq/components/core';
import { KbqCheckedState } from '@koobiq/components/core';
import { KbqColorDirective } from '@koobiq/components/core';
import { OnDestroy } from '@angular/core';
import { Provider } from '@angular/core';
+import { TransitionCheckState } from '@koobiq/components/core';
// @public
-export const KBQ_CHECKBOX_CLICK_ACTION: InjectionToken;
+export const KBQ_CHECKBOX_CLICK_ACTION: InjectionToken;
-// @public
+// @public @deprecated
export const KBQ_CHECKBOX_CONTROL_VALUE_ACCESSOR: any;
// @public (undocumented)
@@ -58,23 +61,24 @@ export class KbqCheckbox extends KbqColorDirective implements ControlValueAccess
// (undocumented)
onInteractionEvent(event: Event): void;
onLabelTextChange(): void;
+ // @deprecated
onTouched: () => any;
- // (undocumented)
+ // @deprecated
registerOnChange(fn: (value: any) => void): void;
- // (undocumented)
+ // @deprecated
registerOnTouched(fn: any): void;
readonly required: _angular_core.InputSignalWithTransform;
- // (undocumented)
+ // @deprecated
setDisabledState(isDisabled: boolean): void;
// (undocumented)
get tabIndex(): number;
set tabIndex(value: number);
toggle(): void;
readonly value: _angular_core.InputSignal;
- // (undocumented)
+ // @deprecated
writeValue(value: any): void;
// (undocumented)
- static ɵcmp: _angular_core.ɵɵComponentDeclaration;
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration;
}
@@ -86,7 +90,7 @@ export class KbqCheckboxChange {
}
// @public
-export type KbqCheckboxClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;
+export type KbqCheckboxClickAction = KbqCheckableClickAction;
// @public (undocumented)
export class KbqCheckboxModule {
@@ -106,13 +110,7 @@ export class KbqCheckboxRequiredValidator extends CheckboxRequiredValidator {
static ɵfac: _angular_core.ɵɵFactoryDeclaration;
}
-// @public
-export enum TransitionCheckState {
- Checked = "checked",
- Indeterminate = "indeterminate",
- Init = "init",
- Unchecked = "unchecked"
-}
+export { TransitionCheckState }
// (No @packageDocumentation comment for this package)
diff --git a/tools/public_api_guard/components/core.api.md b/tools/public_api_guard/components/core.api.md
index bec0b7e218..02c72a953d 100644
--- a/tools/public_api_guard/components/core.api.md
+++ b/tools/public_api_guard/components/core.api.md
@@ -20,6 +20,7 @@ import { ChangeDetectorRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
import { ConnectionPositionPair } from '@angular/cdk/overlay';
+import { ControlValueAccessor } from '@angular/forms';
import { DateAdapter as DateAdapter_2 } from '@koobiq/date-adapter';
import { DateFormats } from '@koobiq/date-adapter';
import { DateFormatter as DateFormatter_2 } from '@koobiq/date-formatter';
@@ -1134,6 +1135,12 @@ export const K = 75;
// @public
export const KBQ_A11Y_LOCALE_CONFIGURATION: InjectionToken;
+// @public
+export const KBQ_CHECKABLE_CLICK_ACTION: InjectionToken;
+
+// @public
+export const KBQ_CHECKABLE_CONTROL_VALUE_ACCESSOR: Provider;
+
// @public
export const KBQ_CONNECTED_OVERLAY_ABOVE_CLASS = "kbq-connected-overlay_above";
@@ -2617,6 +2624,46 @@ export interface KbqBaseFileUploadLocaleConfig {
captionTextWithFolder: string;
}
+// @public
+export class KbqCheckable implements ControlValueAccessor {
+ // (undocumented)
+ readonly checked: i0.ModelSignal;
+ readonly currentCheckState: i0.WritableSignal;
+ // (undocumented)
+ readonly disabled: i0.ModelSignal;
+ readonly effectiveTabIndex: i0.Signal;
+ getAriaChecked(): KbqCheckedState;
+ // (undocumented)
+ readonly indeterminate: i0.ModelSignal;
+ notifyFormValueChange(value: boolean): void;
+ onTouched: () => any;
+ registerOnChange(fn: (value: any) => void): void;
+ // (undocumented)
+ registerOnTouched(fn: any): void;
+ resetNativeInput(nativeInput: HTMLInputElement): void;
+ resolveClick(clickAction: KbqCheckableClickAction): KbqCheckableClickResult;
+ // (undocumented)
+ setDisabledState(isDisabled: boolean): void;
+ readonly tabIndex: i0.ModelSignal;
+ toggle(): void;
+ transitionCheckState(newState: TransitionCheckState): void;
+ // (undocumented)
+ writeValue(value: any): void;
+ // (undocumented)
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+ // (undocumented)
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+}
+
+// @public
+export type KbqCheckableClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;
+
+// @public
+export interface KbqCheckableClickResult {
+ readonly shouldClearIndeterminate: boolean;
+ readonly shouldToggle: boolean;
+}
+
// @public
export type KbqCheckedState = 'true' | 'false' | 'mixed';
@@ -5311,6 +5358,14 @@ export const TOP_POSITION_PRIORITY: ConnectionPositionPair[];
// @public (undocumented)
export const TOP_RIGHT_POSITION_PRIORITY: ConnectionPositionPair[];
+// @public
+export enum TransitionCheckState {
+ Checked = "checked",
+ Indeterminate = "indeterminate",
+ Init = "init",
+ Unchecked = "unchecked"
+}
+
// @public (undocumented)
export const TWO = 50;
diff --git a/tools/public_api_guard/components/toggle.api.md b/tools/public_api_guard/components/toggle.api.md
index 2e00b9846f..4484f25a2a 100644
--- a/tools/public_api_guard/components/toggle.api.md
+++ b/tools/public_api_guard/components/toggle.api.md
@@ -8,12 +8,13 @@ import { AfterViewInit } from '@angular/core';
import * as _angular_core from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { ElementRef } from '@angular/core';
-import * as i1 from '@angular/cdk/a11y';
-import { KbqCheckboxClickAction } from '@koobiq/components/checkbox';
+import * as i1$1 from '@angular/cdk/a11y';
+import * as i1 from '@koobiq/components/core';
+import { KbqCheckableClickAction } from '@koobiq/components/core';
import { KbqCheckedState } from '@koobiq/components/core';
import { KbqColorDirective } from '@koobiq/components/core';
import { OnDestroy } from '@angular/core';
-import { TransitionCheckState } from '@koobiq/components/checkbox';
+import { TransitionCheckState } from '@koobiq/components/core';
// @public (undocumented)
export class KbqToggleChange {
@@ -23,6 +24,9 @@ export class KbqToggleChange {
source: KbqToggleComponent;
}
+// @public
+export type KbqToggleClickAction = KbqCheckableClickAction;
+
// @public (undocumented)
export class KbqToggleComponent extends KbqColorDirective implements AfterViewInit, ControlValueAccessor, OnDestroy {
constructor();
@@ -37,7 +41,7 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
// (undocumented)
get checked(): boolean;
set checked(value: boolean);
- clickAction: KbqCheckboxClickAction;
+ clickAction: KbqToggleClickAction;
protected currentCheckState: TransitionCheckState;
// (undocumented)
get disabled(): any;
@@ -76,21 +80,21 @@ export class KbqToggleComponent extends KbqColorDirective implements AfterViewIn
onInputClick(event: MouseEvent): void;
// (undocumented)
onLabelTextChange(): void;
- // (undocumented)
+ // @deprecated
registerOnChange(fn: any): void;
- // (undocumented)
+ // @deprecated
registerOnTouched(fn: any): void;
- // (undocumented)
+ // @deprecated
setDisabledState(isDisabled: boolean): void;
// (undocumented)
get tabIndex(): number;
set tabIndex(value: number);
// (undocumented)
readonly value: _angular_core.InputSignal;
- // (undocumented)
+ // @deprecated
writeValue(value: any): void;
// (undocumented)
- static ɵcmp: _angular_core.ɵɵComponentDeclaration;
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration;
}
@@ -102,7 +106,7 @@ export class KbqToggleModule {
// (undocumented)
static ɵinj: _angular_core.ɵɵInjectorDeclaration;
// (undocumented)
- static ɵmod: _angular_core.ɵɵNgModuleDeclaration;
+ static ɵmod: _angular_core.ɵɵNgModuleDeclaration;
}
// (No @packageDocumentation comment for this package)