Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/src/app/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ const structure: DocsStructure = makeStructure({
svgPreview: 'checkbox',
hasApi: true,
apiId: 'checkbox',
hasExamples: false
hasExamples: true
},
{
id: DocsStructureItemId.ClampedList,
Expand Down
2 changes: 2 additions & 0 deletions packages/components-dev/checkbox/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { DevThemeToggle } from '../theme-toggle';
<pseudo-checkbox-example />
<hr />
<checkbox-multiline-example />
<hr />
<block-checkbox-example />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Expand Down
3 changes: 2 additions & 1 deletion packages/components/checkbox/checkbox-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core';
import { KbqCheckableClickAction } from '@koobiq/components/core';

/**
* Checkbox click action when user click on input element.
Expand All @@ -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.
Expand Down
165 changes: 71 additions & 94 deletions packages/components/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,32 @@ 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 };
Comment thread
NikGurev marked this conversation as resolved.

// Increasing integer for generating unique ids for checkbox components.
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,
useExisting: forwardRef(() => KbqCheckbox),
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. */
Expand All @@ -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: {
Expand All @@ -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<boolean>(false);

Expand Down Expand Up @@ -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();
Comment thread
NikGurev marked this conversation as resolved.
}

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
Expand All @@ -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();

Expand All @@ -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 = () => {};

Expand All @@ -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();
}

/**
Expand All @@ -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);
}
}

Expand All @@ -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();
}
}
}
6 changes: 3 additions & 3 deletions packages/components/checkbox/examples.checkbox.en.md
Original file line number Diff line number Diff line change
@@ -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.
<!-- example(block-checkbox) -->
6 changes: 3 additions & 3 deletions packages/components/checkbox/examples.checkbox.ru.md
Comment thread
NikGurev marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
🚧 **Документация в процессе написания** 🚧
### Блочный чекбокс

К сожалению, документация для этого раздела еще не готова. Мы активно работаем над ее созданием и планируем добавить в ближайшее время.
Кастомный чекбокс-подобный элемент в виде карточки, построенный на общем примитиве `KbqCheckable` (`@koobiq/components/core`) — демонстрирует повторное использование логики клика, форм и доступности с собственной разметкой и стилями.

Если вы хотите помочь в написании документации или у вас есть вопросы, пожалуйста, [создайте issue](https://github.com/koobiq/angular-components/issues) в нашем репозитории на GitHub.
<!-- example(block-checkbox) -->
Loading