@if (canShowCleaner) {
-
+
}
diff --git a/packages/components/form-field/form-field.spec.ts b/packages/components/form-field/form-field.spec.ts
index 99e8883c1d..e02a163c8d 100644
--- a/packages/components/form-field/form-field.spec.ts
+++ b/packages/components/form-field/form-field.spec.ts
@@ -12,6 +12,8 @@ import {
import { By } from '@angular/platform-browser';
import {
ErrorStateMatcher,
+ ESCAPE,
+ KBQ_FORM_FIELD_REF,
PasswordValidators,
ShowOnFormSubmitErrorStateMatcher,
ShowRequiredOnSubmitErrorStateMatcher
@@ -20,7 +22,12 @@ import { KbqInput, KbqInputModule, KbqInputPassword } from '@koobiq/components/i
import { Subject } from 'rxjs';
import { KbqCleaner } from './cleaner';
import { KbqError } from './error';
-import { getKbqFormFieldMissingControlError, KbqFormField, kbqFormFieldDefaultOptionsProvider } from './form-field';
+import {
+ getKbqFormFieldMissingControlError,
+ KBQ_FORM_FIELD,
+ KbqFormField,
+ kbqFormFieldDefaultOptionsProvider
+} from './form-field';
import { KbqFormFieldModule } from './form-field.module';
import { KbqHint } from './hint';
import { KbqLabel } from './label';
@@ -339,6 +346,14 @@ class PasswordFormFieldWithConditionalContent {
}
describe(KbqFormField.name, () => {
+ it('should provide typed and legacy form-field tokens', () => {
+ const { debugElement } = createComponent(InputFormFieldWithHintAndError);
+ const formField = getFormFieldDebugElement(debugElement);
+
+ expect(formField.injector.get(KBQ_FORM_FIELD)).toBe(formField.componentInstance);
+ expect(formField.injector.get(KBQ_FORM_FIELD_REF)).toBe(formField.componentInstance);
+ });
+
it('should display KbqHint', () => {
const { debugElement } = createComponent(InputFormFieldWithHintAndError);
const hint = getHintDebugElement(debugElement).nativeElement;
@@ -428,6 +443,32 @@ describe(KbqFormField.name, () => {
expect(componentInstance.control.value).toBeNull();
});
+ it('should clean field by KbqCleaner on Space', () => {
+ const { debugElement, componentInstance } = createComponent(InputFormFieldWithCleaner);
+ const input = getInputNativeElement(debugElement);
+
+ input.value = 'koobiq';
+ input.dispatchEvent(new Event('input'));
+
+ getCleanerDebugElement(debugElement).nativeElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: ' ', bubbles: true })
+ );
+
+ expect(componentInstance.control.value).toBeNull();
+ });
+
+ it('should clean focused field by KbqCleaner on Escape', () => {
+ const { debugElement, componentInstance } = createComponent(InputFormFieldWithCleaner);
+ const input = getInputNativeElement(debugElement);
+
+ input.value = 'koobiq';
+ input.dispatchEvent(new Event('input'));
+ input.focus();
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: ESCAPE, bubbles: true }));
+
+ expect(componentInstance.control.value).toBeNull();
+ });
+
it('should apply kbq-error class to KbqCleaner icon when control is invalid', () => {
const { debugElement } = createComponent(InputFormFieldWithCleanerAndCustomErrorStateMatcher);
diff --git a/packages/components/form-field/form-field.ts b/packages/components/form-field/form-field.ts
index b0ae3b10c9..4a80291ef4 100644
--- a/packages/components/form-field/form-field.ts
+++ b/packages/components/form-field/form-field.ts
@@ -28,9 +28,10 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgControl } from '@angular/forms';
import { KBQ_CONNECTED_OVERLAY_ORIGIN, KBQ_FORM_FIELD_REF, KbqColorDirective } from '@koobiq/components/core';
+import { kbqIconErrorStateContextFactoryProvider } from '@koobiq/components/icon';
import { EMPTY, merge } from 'rxjs';
import { delay, startWith } from 'rxjs/operators';
-import { KbqCleaner } from './cleaner';
+import { KbqCleaner, kbqCleanerFactoryProvider } from './cleaner';
import { KbqError } from './error';
import { KbqFormFieldControl, kbqSetDescribedByIds } from './form-field-control';
import { KbqHint } from './hint';
@@ -39,7 +40,7 @@ import { hasPasswordStrengthError, KbqPasswordHint } from './password-hint';
import { KbqPasswordToggle } from './password-toggle';
import { KbqPrefix } from './prefix';
import { KbqReactivePasswordHint } from './reactive-password-hint';
-import { KbqNumberInputControl, KbqStepper } from './stepper';
+import { KbqStepper } from './stepper';
import { KbqSuffix } from './suffix';
/** @docs-private */
@@ -47,10 +48,11 @@ export function getKbqFormFieldMissingControlError(): Error {
return Error('kbq-form-field must contain a KbqFormFieldControl');
}
-/** @docs-private */
-export function getKbqFormFieldYouCanNotUseCleanerInNumberInputError(): Error {
- return Error(`You can't use kbq-cleaner with input that have type="number"`);
-}
+/**
+ * Injection token used to access the owning `KbqFormField`.
+ * @docs-private
+ */
+export const KBQ_FORM_FIELD = new InjectionToken
('KbqFormField');
/** Error key set on the control by the legacy `KbqPasswordHint` when the password is not strong enough. */
const PASSWORD_STRENGTH_ERROR = 'passwordStrength';
@@ -74,6 +76,7 @@ export type KbqFormFieldDefaultOptions = Partial<{
/**
* Injection token that can be used to configure the default options for all kbq-form-field's.
+ * @docs-private
*/
export const KBQ_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken(
'KBQ_FORM_FIELD_DEFAULT_OPTIONS'
@@ -108,7 +111,36 @@ export const kbqFormFieldDefaultOptionsProvider = (options: KbqFormFieldDefaultO
// KbqTagInput
'../tags/tag-input-tokens.scss'
],
- providers: [{ provide: KBQ_FORM_FIELD_REF, useExisting: KbqFormField }],
+ providers: [
+ // Kept for consumers that cannot depend on the form-field entry point.
+ { provide: KBQ_FORM_FIELD_REF, useExisting: KbqFormField },
+ { provide: KBQ_FORM_FIELD, useExisting: KbqFormField },
+ kbqCleanerFactoryProvider(() => {
+ const formField = inject(KBQ_FORM_FIELD);
+
+ return {
+ get control() {
+ return formField.control();
+ },
+ get keydownTarget() {
+ return formField.elementRef.nativeElement;
+ },
+ clearByEscape: true
+ };
+ }),
+ kbqIconErrorStateContextFactoryProvider(() => {
+ const formField = inject(KBQ_FORM_FIELD);
+
+ return {
+ get errorState() {
+ return formField.control().errorState;
+ },
+ get stateChanges() {
+ return formField.control().stateChanges;
+ }
+ };
+ })
+ ],
changeDetection: ChangeDetectionStrategy.OnPush,
// Every component of this package renders unencapsulated, on purpose: the `kbq-form-field__*` and
// `kbq-hint*` classes are a public contract that themes, the controls projected into the field and
@@ -221,6 +253,7 @@ export class KbqFormField
/**
* @docs-private
+ * @deprecated Unused. This property is no longer used by the form field and will be removed in a future version.
*/
canCleanerClearByEsc: boolean = true;
@@ -324,15 +357,7 @@ export class KbqFormField
* @docs-private
*/
get canShowCleaner(): boolean {
- const ngControl = this.control()?.ngControl;
-
- if (!this.hasCleaner() || !ngControl) {
- return false;
- }
-
- const { value } = ngControl;
-
- return value !== null && value !== undefined && value !== '' && !this.disabled;
+ return !!this.cleaner()?.canShow;
}
/** Whether the form field is disabled. */
@@ -346,10 +371,6 @@ export class KbqFormField
ngAfterContentInit(): void {
this.validateControlChild();
- if (this.numberInput() && this.hasCleaner()) {
- throw getKbqFormFieldYouCanNotUseCleanerInNumberInputError();
- }
-
// Subscribe to changes in the child control state in order to update the form field UI.
this.control()
.stateChanges.pipe(startWith(), delay(0), takeUntilDestroyed(this.destroyRef))
@@ -361,10 +382,6 @@ export class KbqFormField
}
});
- if (this.hasStepper()) {
- this.stepper()!.connectTo(this.numberInput()!);
- }
-
this.initializeControl();
}
@@ -392,17 +409,7 @@ export class KbqFormField
* @docs-private
*/
clearValue(event: Event): void {
- event.stopPropagation();
-
- // Keyboard activation of the cleaner must not also scroll the page (Space) or submit the form (Enter).
- if (event.type === 'keydown') {
- event.preventDefault();
- }
-
- const control = this.control();
-
- control?.ngControl?.reset();
- control?.focus();
+ this.cleaner()?.clear(event);
}
/**
@@ -420,22 +427,10 @@ export class KbqFormField
/**
* Handles keydown events.
- *
+ * @deprecated Unused. This method is no longer called by the form field and will be removed in a future version.
* @docs-private
*/
- onKeyDown(event: KeyboardEvent): void {
- const control = this.control();
-
- if (control.controlType === 'input-password' && event.altKey && event.key === 'F8') {
- (control as unknown as { toggleType(): void }).toggleType();
- }
-
- if (this.canCleanerClearByEsc && event.key === 'Escape' && control.focused && this.hasCleaner()) {
- control?.ngControl?.reset();
-
- event.preventDefault();
- }
- }
+ onKeyDown(_event: KeyboardEvent): void {}
/**
* @docs-private
@@ -505,11 +500,6 @@ export class KbqFormField
}
}
- /** Resolves the number input hosted by the control, `null` when the control is not a `kbqNumberInput`. */
- private numberInput(): KbqNumberInputControl | null {
- return (this.control() as unknown as { numberInput?: KbqNumberInputControl }).numberInput || null;
- }
-
/**
* Links the hints and the error rendered by the form field to the control via `aria-describedby`,
* so assistive technology announces them when the control gets focused.
diff --git a/packages/components/form-field/password-toggle.ts b/packages/components/form-field/password-toggle.ts
index ec3de93e03..2eea358037 100644
--- a/packages/components/form-field/password-toggle.ts
+++ b/packages/components/form-field/password-toggle.ts
@@ -1,5 +1,6 @@
import { FocusMonitor } from '@angular/cdk/a11y';
+import { F8 } from '@angular/cdk/keycodes';
import {
AfterContentInit,
afterNextRender,
@@ -17,31 +18,40 @@ import {
ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import {
- KBQ_FORM_FIELD_REF,
- kbqInjectA11yLocaleConfiguration,
- kbqInjectNativeElement,
- PopUpTriggers
-} from '@koobiq/components/core';
+import { kbqInjectA11yLocaleConfiguration, kbqInjectNativeElement, PopUpTriggers } from '@koobiq/components/core';
import { KbqIconButton, KbqIconModule } from '@koobiq/components/icon';
import { KbqToolTipModule, KbqTooltipTrigger } from '@koobiq/components/tooltip';
-import { EMPTY } from 'rxjs';
-import { KbqFormField } from './form-field';
+import { EMPTY, fromEvent } from 'rxjs';
+import { KBQ_FORM_FIELD } from './form-field';
import { KbqFormFieldControl } from './form-field-control';
-// @TODO Temporary solution to resolve circular dependency (#DS-3893)
-type KbqInputPassword = KbqFormFieldControl & {
- elementType: string;
+/**
+ * Narrow structural contract for `kbqInputPassword`, duck-typed here to avoid a circular
+ * dependency between `@koobiq/components/form-field` and `@koobiq/components/input`.
+ */
+type KbqPasswordToggleControl = KbqFormFieldControl & {
+ readonly controlType: 'input-password';
+ elementType: 'text' | 'password';
toggleType: () => void;
};
-// @TODO Temporary solution to resolve circular dependency (#DS-3893)
-const isInputPassword = (control: KbqFormFieldControl): control is KbqInputPassword => {
- return 'elementType' in control;
+/**
+ * Checks whether the given control structurally matches `KbqPasswordToggleControl`.
+ */
+const isPasswordToggleControl = (control: unknown): control is KbqPasswordToggleControl => {
+ return (
+ !!control &&
+ typeof control === 'object' &&
+ 'controlType' in control &&
+ control.controlType === 'input-password' &&
+ 'elementType' in control &&
+ 'toggleType' in control &&
+ typeof control.toggleType === 'function'
+ );
};
const getKbqPasswordToggleMissingControlError = (): Error => {
- return Error('kbq-password-toggle should use with kbqInputPassword');
+ return Error('You should use kbq-password-toggle with kbqInputPassword');
};
/** Component which changes password visibility. */
@@ -82,9 +92,7 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
protected readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly a11yLocaleConfiguration = kbqInjectA11yLocaleConfiguration();
-
- // @TODO fix types (#DS-2915)
- private readonly formField = inject(KBQ_FORM_FIELD_REF, { optional: true }) as unknown as KbqFormField | undefined;
+ private readonly formField = inject(KBQ_FORM_FIELD, { optional: true });
readonly tabindex = input(0, { transform: numberAttribute });
@@ -116,10 +124,10 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
protected hasError: boolean = false;
/** Form field password control. */
- private get control(): KbqInputPassword {
+ private get control(): KbqPasswordToggleControl {
const control = this.formField?.control();
- if (!control || !isInputPassword(control)) {
+ if (!isPasswordToggleControl(control)) {
throw getKbqPasswordToggleMissingControlError();
}
@@ -163,6 +171,14 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
this.trigger = `${PopUpTriggers.Hover}`;
+ const keydownTarget = this.formField?.elementRef.nativeElement;
+
+ if (keydownTarget) {
+ fromEvent(keydownTarget, 'keydown')
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((event) => this.onFormFieldKeyDown(event));
+ }
+
// `stateChanges` is owned by the control and outlives the toggle, so the subscription has to be torn
// down explicitly. Subscribing after render also keeps it off the server, matching KbqReactivePasswordHint.
afterNextRender(() => {
@@ -172,23 +188,14 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
});
}
- /**
- * @docs-private
- */
ngAfterContentInit(): void {
this.updateState();
}
- /**
- * @docs-private
- */
ngAfterViewInit(): void {
this.focusMonitor.monitor(this.nativeElement, true);
}
- /**
- * @docs-private
- */
ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.nativeElement);
}
@@ -196,7 +203,9 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
/**
* @docs-private
*/
- toggle(event: Event) {
+ toggle(event: Event): void {
+ if (this.control.disabled) return;
+
this.hide();
this.control.toggleType();
@@ -206,8 +215,17 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
event.preventDefault();
}
+ /**
+ * Toggles password visibility for the Alt+F8 shortcut.
+ */
+ private onFormFieldKeyDown(event: KeyboardEvent): void {
+ if (event.altKey && event.keyCode === F8) {
+ this.toggle(event);
+ }
+ }
+
private updateState = () => {
- this.hasError = !!this.formField?.control()?.errorState;
+ this.hasError = !!this.control.errorState;
this.changeDetectorRef.markForCheck();
};
diff --git a/packages/components/form-field/reactive-password-hint.ts b/packages/components/form-field/reactive-password-hint.ts
index 0194131f59..c26664a298 100644
--- a/packages/components/form-field/reactive-password-hint.ts
+++ b/packages/components/form-field/reactive-password-hint.ts
@@ -12,11 +12,11 @@ import {
ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { KBQ_FORM_FIELD_REF, KbqComponentColors } from '@koobiq/components/core';
+import { KbqComponentColors } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { EMPTY } from 'rxjs';
import { delay } from 'rxjs/operators';
-import { KbqFormField } from './form-field';
+import { KBQ_FORM_FIELD } from './form-field';
import { KbqHint } from './hint';
/** Password hint to be shown below the password form field control. */
@@ -42,8 +42,7 @@ import { KbqHint } from './hint';
exportAs: 'kbqReactivePasswordHint'
})
export class KbqReactivePasswordHint extends KbqHint {
- // @TODO fix types (#DS-2915)
- private readonly formField = inject(KBQ_FORM_FIELD_REF, { optional: true });
+ private readonly formField = inject(KBQ_FORM_FIELD, { optional: true });
private readonly destroyRef = inject(DestroyRef);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
diff --git a/packages/components/form-field/stepper.ts b/packages/components/form-field/stepper.ts
index 76273ab47e..2948c4d4d7 100644
--- a/packages/components/form-field/stepper.ts
+++ b/packages/components/form-field/stepper.ts
@@ -10,33 +10,42 @@ import {
ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { KBQ_FORM_FIELD_REF } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { concatMap, fromEvent, interval, Subject, timer } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
-import { KbqFormFieldControl } from './form-field-control';
+import { KBQ_FORM_FIELD } from './form-field';
+import type { KbqFormFieldControl } from './form-field-control';
/**
- * The subset of `KbqNumberInput` the stepper drives.
- *
- * Declared here instead of imported from `@koobiq/components/input` to break the circular dependency
- * between the packages (#DS-3893).
+ * Narrow structural contract for the control hosted by `kbqNumberInput`, duck-typed here to avoid
+ * a circular dependency between `@koobiq/components/form-field` and `@koobiq/components/input`.
*/
-export interface KbqNumberInputControl extends KbqFormFieldControl {
- /** Increases the value by `step`. */
+type KbqStepperControl = KbqFormFieldControl & {
+ readonly controlType: 'input-number';
+ readonly step: number;
stepUp: (step: number) => void;
/** Decreases the value by `step`. */
stepDown: (step: number) => void;
- /** Amount the value changes by on a single step. */
- step: number;
-}
+};
-const isNumberInput = (control: KbqFormFieldControl): control is KbqNumberInputControl => {
- return 'stepUp' in control && 'stepDown' in control;
+/**
+ * Checks whether the given value structurally matches `KbqStepperControl`.
+ */
+const isStepperControl = (control: unknown): control is KbqStepperControl => {
+ return (
+ !!control &&
+ typeof control === 'object' &&
+ 'controlType' in control &&
+ control.controlType === 'input-number' &&
+ 'stepUp' in control &&
+ typeof control.stepUp === 'function' &&
+ 'stepDown' in control &&
+ typeof control.stepDown === 'function'
+ );
};
const getKbqStepperToggleMissingControlError = (): Error => {
- return Error('kbq-stepper should use with kbqNumberInput');
+ return Error('You should use kbq-stepper with kbqNumberInput');
};
/**
@@ -86,8 +95,8 @@ export const KBQ_STEPPER_INTERVAL_DELAY = 75;
exportAs: 'kbqStepper'
})
export class KbqStepper {
- private readonly formField = inject(KBQ_FORM_FIELD_REF, { optional: true });
- private readonly document = inject(DOCUMENT);
+ private readonly formField = inject(KBQ_FORM_FIELD, { optional: true });
+ private readonly document = inject(DOCUMENT);
private readonly destroyRef = inject(DestroyRef);
/** Emitted when the stepper is incremented. */
@@ -115,23 +124,24 @@ export class KbqStepper {
takeUntil(this.mouseUp)
);
- /** Whether `connectTo` has already wired the number input. */
- private connected = false;
-
/**
* Form field number control. Resolved once per control change instead of on every template read.
*
* @docs-private
*/
- protected readonly control = computed(() => {
+ protected readonly control = computed(() => {
const control = this.formField?.control();
- const input = (control as { numberInput?: KbqFormFieldControl })?.numberInput;
+ // `formFieldControl` here is `KbqInput` (it owns the `KbqFormFieldControl` provider on
+ // ``), not the number-input itself: `stepUp`/`stepDown`/`step`
+ // live on the sibling `KbqNumberInput` directive, reachable only via `KbqInput.numberInput`.
+ const inputNumberControl =
+ control?.controlType === 'input-number' && 'numberInput' in control ? control.numberInput : null;
- if (!input || !isNumberInput(input)) {
+ if (!isStepperControl(inputNumberControl)) {
throw getKbqStepperToggleMissingControlError();
}
- return input;
+ return inputNumberControl;
});
constructor() {
@@ -139,42 +149,41 @@ export class KbqStepper {
}
/**
+ * @deprecated No longer required — `KbqStepper` resolves its control automatically via DI.
* @docs-private
*/
- connectTo(numberInput: KbqNumberInputControl): void {
- // The form field calls it on every content init, and the outputs must not be wired twice.
- if (!numberInput || this.connected) return;
-
- this.connected = true;
-
- this.stepUp.subscribe(() => {
- numberInput.stepUp(numberInput.step);
- });
-
- this.stepDown.subscribe(() => {
- numberInput.stepDown(numberInput.step);
- });
- }
+ connectTo(_: any): void {}
/** @docs-private */
- onStepUp($event: MouseEvent): void {
- this.handleStep($event, this.stepUp);
+ onStepUp(event: MouseEvent): void {
+ this.handleStep(event, (control) => control.stepUp(control.step), this.stepUp);
}
/** @docs-private */
- onStepDown($event: MouseEvent): void {
- this.handleStep($event, this.stepDown);
+ onStepDown(event: MouseEvent): void {
+ this.handleStep(event, (control) => control.stepDown(control.step), this.stepDown);
}
- private handleStep($event: MouseEvent, emitter: OutputEmitterRef): void {
- if (this.control().disabled) return;
+ private handleStep(
+ event: MouseEvent,
+ step: (control: KbqStepperControl) => void,
+ emitter: OutputEmitterRef
+ ): void {
+ const control = this.control();
+
+ if (control.disabled) return;
+
+ const performStep = () => {
+ step(control);
+ emitter.emit();
+ };
- emitter.emit();
+ performStep();
// handle case when cursor is out of viewport.
fromEvent(this.document, 'mouseup')
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.mouseUp.next());
- this.longPress.subscribe(() => emitter.emit());
- $event.preventDefault();
+ this.longPress.subscribe(performStep);
+ event.preventDefault();
}
}
diff --git a/packages/components/icon/icon-error-state-context.ts b/packages/components/icon/icon-error-state-context.ts
new file mode 100644
index 0000000000..889f3f36b0
--- /dev/null
+++ b/packages/components/icon/icon-error-state-context.ts
@@ -0,0 +1,29 @@
+import { InjectionToken, Provider } from '@angular/core';
+import type { Observable } from 'rxjs';
+
+/**
+ * Narrow contract that lets `KbqIcon` react to a host's error state (e.g. `autoColor`) without
+ * depending on the host's concrete class.
+ * @docs-private
+ */
+export interface KbqIconErrorStateContext {
+ /** Whether the host is currently in an error state. */
+ readonly errorState: boolean;
+ /** Emits whenever the host's state changes, prompting `KbqIcon` to re-check `errorState`. */
+ readonly stateChanges: Observable;
+}
+
+/**
+ * Injection token used by `KbqIcon` to look up its `KbqIconErrorStateContext`.
+ * @docs-private
+ */
+export const KBQ_ICON_ERROR_STATE_CONTEXT = new InjectionToken('KbqIconErrorStateContext');
+
+/**
+ * Utility provider for `KBQ_ICON_ERROR_STATE_CONTEXT`, built from a factory that resolves the current host's `KbqIconErrorStateContext`.
+ * @docs-private
+ */
+export const kbqIconErrorStateContextFactoryProvider = (factory: () => KbqIconErrorStateContext): Provider => ({
+ provide: KBQ_ICON_ERROR_STATE_CONTEXT,
+ useFactory: factory
+});
diff --git a/packages/components/icon/icon.component.ts b/packages/components/icon/icon.component.ts
index 0d799c5365..72c2401e60 100644
--- a/packages/components/icon/icon.component.ts
+++ b/packages/components/icon/icon.component.ts
@@ -13,9 +13,10 @@ import {
ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { KBQ_FORM_FIELD_REF, KbqColorDirective } from '@koobiq/components/core';
+import { KbqColorDirective } from '@koobiq/components/core';
import { EMPTY, ReplaySubject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
+import { KBQ_ICON_ERROR_STATE_CONTEXT } from './icon-error-state-context';
import { KbqIconRegistry } from './icon-registry';
@Component({
@@ -33,7 +34,11 @@ import { KbqIconRegistry } from './icon-registry';
export class KbqIcon extends KbqColorDirective implements AfterContentInit, OnChanges {
readonly elementRef = inject>(ElementRef);
- protected readonly formField = inject(KBQ_FORM_FIELD_REF, { optional: true });
+ /**
+ * Host providing error state for `autoColor`, when the icon sits inside one (e.g. a form field).
+ * @docs-private
+ */
+ protected readonly errorStateContext = inject(KBQ_ICON_ERROR_STATE_CONTEXT, { optional: true });
protected readonly changeDetectorRef = inject(ChangeDetectorRef);
protected readonly registry = inject(KbqIconRegistry, { optional: true });
protected readonly destroyRef = inject(DestroyRef);
@@ -98,7 +103,7 @@ export class KbqIcon extends KbqColorDirective implements AfterContentInit, OnCh
ngAfterContentInit(): void {
if (this.autoColor) {
- this.formField?.control()?.stateChanges.subscribe(this.updateState);
+ this.errorStateContext?.stateChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(this.updateState);
this.updateState();
}
@@ -149,7 +154,7 @@ export class KbqIcon extends KbqColorDirective implements AfterContentInit, OnCh
}
private updateState = () => {
- this.hasError = !!this.formField?.control()?.errorState;
+ this.hasError = !!this.errorStateContext?.errorState;
this.changeDetectorRef.markForCheck();
};
diff --git a/packages/components/icon/public-api.ts b/packages/components/icon/public-api.ts
index 9ed421379b..a4aafc0654 100644
--- a/packages/components/icon/public-api.ts
+++ b/packages/components/icon/public-api.ts
@@ -1,4 +1,5 @@
export * from './icon-button.component';
+export * from './icon-error-state-context';
export * from './icon-item.component';
export * from './icon-registry';
export * from './icon-registry-providers';
diff --git a/packages/components/inline-edit/inline-edit.spec.ts b/packages/components/inline-edit/inline-edit.spec.ts
index 2b8e1cb743..6a85bc2523 100644
--- a/packages/components/inline-edit/inline-edit.spec.ts
+++ b/packages/components/inline-edit/inline-edit.spec.ts
@@ -1218,7 +1218,7 @@ export class TestWithFullTabOrder {
-
+
@for (option of options; track option) {
{{ option }}
}
diff --git a/packages/components/input/__screenshots__/04-light.png b/packages/components/input/__screenshots__/04-light.png
index 1db569eb02..8346ece5e1 100644
Binary files a/packages/components/input/__screenshots__/04-light.png and b/packages/components/input/__screenshots__/04-light.png differ
diff --git a/packages/components/input/e2e.playwright-spec.ts b/packages/components/input/e2e.playwright-spec.ts
index ba2739c521..4ca27f6935 100644
--- a/packages/components/input/e2e.playwright-spec.ts
+++ b/packages/components/input/e2e.playwright-spec.ts
@@ -47,6 +47,8 @@ test.describe('KbqInputModule', () => {
await page.goto('/E2eInputStateAndStyle');
const locator = getComponent(page);
const screenshotTarget = getInputPasswordTestRow(locator);
+
+ await screenshotTarget.locator('input').focus();
const toggle = screenshotTarget.locator('kbq-password-toggle');
await toggle.click();
diff --git a/packages/components/input/input-number.spec.ts b/packages/components/input/input-number.spec.ts
index 3ed2606a08..804ba919c1 100644
--- a/packages/components/input/input-number.spec.ts
+++ b/packages/components/input/input-number.spec.ts
@@ -104,6 +104,22 @@ class TestNumberInputConditional {
stepDown = jest.fn().mockImplementation(() => false);
}
+@Component({
+ imports: [FormsModule, KbqInputModule],
+ template: `
+
+
+ @if (showStepper) {
+
+ }
+
+ `
+})
+class KbqNumberInputWithDynamicStepper {
+ value: number | null = 10;
+ showStepper = false;
+}
+
@Component({
imports: [
ReactiveFormsModule,
@@ -277,6 +293,22 @@ class KbqNumberInputWithInteger {
}
describe('KbqNumberInput', () => {
+ it('should use input-number control type', fakeAsync(() => {
+ const fixture = createComponent(KbqNumberInputTestComponent);
+
+ fixture.detectChanges();
+ flush();
+
+ const inputElement = fixture.debugElement.query(By.directive(KbqInput));
+ const input = inputElement.injector.get(KbqInput);
+ const numberInput = inputElement.injector.get(KbqNumberInput);
+ const formField = fixture.debugElement.query(By.css('kbq-form-field')).nativeElement;
+
+ expect(input.controlType).toBe('input-number');
+ expect(numberInput.controlType).toBe('input-number');
+ expect(formField.classList).toContain('kbq-form-field-type-input-number');
+ }));
+
it('should have stepper on focus', fakeAsync(() => {
const fixture = createComponent(KbqNumberInputTestComponent);
@@ -309,9 +341,9 @@ describe('KbqNumberInput', () => {
expect(icons.every((icon) => icon.nativeElement.classList.contains('kbq-error'))).toBe(true);
}));
- it('should throw error with stepper', () => {
- // KbqFormField.ngAfterContentInit() throws when it detects a kbq-cleaner inside
- // a number input. Override ComponentFixtureAutoDetect so CD doesn't run inside
+ it('should throw error with cleaner', () => {
+ // KbqCleaner.ngAfterContentInit() throws when it detects a number input.
+ // Override ComponentFixtureAutoDetect so CD doesn't run inside
// TestBed.createComponent — that way the throw originates from our explicit
// fixture.detectChanges() call, where expect-to-throw can capture it.
jest.spyOn(console, 'error').mockImplementation(() => {});
@@ -369,6 +401,24 @@ describe('KbqNumberInput', () => {
expect(fixture.componentInstance.value).toEqual(initialValue);
}));
+ it('should connect a stepper added after form-field initialization', fakeAsync(() => {
+ const fixture = createComponent(KbqNumberInputWithDynamicStepper);
+
+ fixture.detectChanges();
+ flush();
+ fixture.componentInstance.showStepper = true;
+ fixture.detectChanges();
+
+ const stepper = fixture.debugElement.query(By.css('kbq-stepper'));
+ const iconUp = stepper.queryAll(By.css('.kbq-icon'))[0];
+
+ dispatchFakeEvent(iconUp.nativeElement, 'mousedown');
+ fixture.detectChanges();
+ flush();
+
+ expect(fixture.componentInstance.value).toBe(11);
+ }));
+
describe('with long press on stepper', () => {
const initialValue = 0;
diff --git a/packages/components/input/input-number.ts b/packages/components/input/input-number.ts
index cd5d09d3a3..0093177078 100644
--- a/packages/components/input/input-number.ts
+++ b/packages/components/input/input-number.ts
@@ -122,7 +122,11 @@ export class KbqNumberInput implements KbqFormFieldControl, ControlValueAcc
errorState: boolean;
- controlType?: string | undefined;
+ /**
+ * Implemented as part of KbqFormFieldControl.
+ * @docs-private
+ */
+ controlType: string = 'input-number';
/**
* Allows input and pasting of integers only.
diff --git a/packages/components/input/input-password.spec.ts b/packages/components/input/input-password.spec.ts
index 634cee0255..41a23016fc 100644
--- a/packages/components/input/input-password.spec.ts
+++ b/packages/components/input/input-password.spec.ts
@@ -1,4 +1,5 @@
-import { Component, Provider, Type, viewChild } from '@angular/core';
+import { F8 } from '@angular/cdk/keycodes';
+import { Component, Provider, Type, viewChild } from '@angular/core';
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, fakeAsync, flush, tick } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
@@ -142,7 +143,39 @@ class PasswordInputWithReactiveControl {
});
}
+@Component({
+ imports: [KbqInputModule, FormsModule],
+ template: `
+
+
+ @if (showToggle) {
+
+ }
+
+ `
+})
+class PasswordInputWithDynamicToggle {
+ value = 'password';
+ showToggle = false;
+}
+
describe('KbqPasswordInput', () => {
+ it('should handle Alt+F8 only when KbqPasswordToggle is present', () => {
+ const fixture = createComponent(PasswordInputWithDynamicToggle);
+ const input = fixture.debugElement.query(By.directive(KbqInputPassword)).nativeElement as HTMLInputElement;
+ const togglePassword = () =>
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'F8', keyCode: F8, altKey: true, bubbles: true }));
+
+ fixture.detectChanges();
+ togglePassword();
+ expect(input.type).toBe('password');
+
+ fixture.componentInstance.showToggle = true;
+ fixture.detectChanges();
+ togglePassword();
+ expect(input.type).toBe('text');
+ });
+
it('should have toggle', fakeAsync(() => {
const fixture = createComponent(KbqPasswordInputDefault);
diff --git a/packages/components/input/input-password.ts b/packages/components/input/input-password.ts
index 13295d8e92..f1ccf62da3 100644
--- a/packages/components/input/input-password.ts
+++ b/packages/components/input/input-password.ts
@@ -230,8 +230,10 @@ export class KbqInputPassword
this.checkRule.next();
}
- toggleType() {
+ /** Toggles the native input type between password and text. */
+ toggleType(): void {
this.elementType = this.elementType === 'password' ? 'text' : 'password';
+ this.stateChanges.next(null);
}
/** Focuses the input. */
diff --git a/packages/components/input/input.spec.ts b/packages/components/input/input.spec.ts
index b299e8533f..48f57f249e 100644
--- a/packages/components/input/input.spec.ts
+++ b/packages/components/input/input.spec.ts
@@ -299,7 +299,7 @@ describe('KbqInput', () => {
expect(formFieldElement.querySelectorAll('.kbq-form-field__cleaner').length).toBe(1);
- const cleanerElement = fixture.debugElement.query(By.css('.kbq-form-field__cleaner')).nativeElement;
+ const cleanerElement = fixture.debugElement.query(By.css('.kbq-cleaner')).nativeElement;
cleanerElement.click();
fixture.detectChanges();
diff --git a/packages/components/input/input.ts b/packages/components/input/input.ts
index 2e0981cce6..38c6e206f1 100644
--- a/packages/components/input/input.ts
+++ b/packages/components/input/input.ts
@@ -80,7 +80,7 @@ export class KbqInput
* Implemented as part of KbqFormFieldControl.
* @docs-private
*/
- controlType: string = 'input';
+ controlType: string = this.numberInput ? 'input-number' : 'input';
/**
* Implemented as part of KbqFormFieldControl.
diff --git a/packages/components/select/select.component.spec.ts b/packages/components/select/select.component.spec.ts
index 4b3637d833..1720831489 100644
--- a/packages/components/select/select.component.spec.ts
+++ b/packages/components/select/select.component.spec.ts
@@ -63,7 +63,7 @@ import {
kbqErrorStateMatcherProvider,
wrappedErrorMessage
} from '@koobiq/components/core';
-import { KbqFormField, KbqFormFieldModule } from '@koobiq/components/form-field';
+import { KbqCleaner, KbqFormField, KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagsModule } from '@koobiq/components/tags';
@@ -305,7 +305,7 @@ const OPTIONS = [
-
+
@@ -2035,7 +2035,7 @@ class VirtualSelectWithScrolledToBottom {
@for (food of foods; track food) {
{{ food.viewValue }}
}
-
+
`
@@ -2050,6 +2050,22 @@ class MultiSelectWithCleaner {
readonly select = viewChild.required(KbqSelect);
}
+@Component({
+ selector: 'select-with-cleaner-without-template-ref',
+ imports: [KbqSelectModule, ReactiveFormsModule],
+ template: `
+
+
+ Pizza
+
+
+
+ `
+})
+class SelectWithCleanerWithoutTemplateRef {
+ readonly control = new UntypedFormControl('pizza');
+}
+
describe('KbqSelect', () => {
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
@@ -3422,7 +3438,7 @@ describe('KbqSelect', () => {
expect(value.nativeElement.textContent).toContain('Pizza');
- cleaner = fixture.debugElement.query(By.css('.kbq-select__cleaner')).nativeElement;
+ cleaner = fixture.debugElement.query(By.directive(KbqCleaner)).nativeElement;
cleaner.click();
fixture.detectChanges();
flush();
@@ -3437,13 +3453,33 @@ describe('KbqSelect', () => {
expect(fixture.componentInstance.control.value).toBe('pizza-1');
- cleaner = fixture.debugElement.query(By.css('.kbq-select__cleaner')).nativeElement;
+ cleaner = fixture.debugElement.query(By.directive(KbqCleaner)).nativeElement;
cleaner.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBeUndefined();
}));
+
+ it('should discover cleaner by component type without a template reference', fakeAsync(() => {
+ const cleanerFixture = TestBed.createComponent(SelectWithCleanerWithoutTemplateRef);
+
+ cleanerFixture.detectChanges();
+ flush();
+ cleanerFixture.componentInstance.control.setValue('pizza');
+ cleanerFixture.detectChanges();
+ flush();
+
+ const cleaner = cleanerFixture.debugElement.query(By.directive(KbqCleaner));
+
+ expect(cleanerFixture.debugElement.query(By.directive(KbqCleaner))).not.toBeNull();
+ expect(cleaner).not.toBeNull();
+ cleaner.nativeElement.click();
+ cleanerFixture.detectChanges();
+ flush();
+
+ expect(cleanerFixture.componentInstance.control.value).toBeUndefined();
+ }));
});
describe('Clear value — multiple select', () => {
@@ -3459,7 +3495,7 @@ describe('KbqSelect', () => {
it('should update FormControl value to empty array after clear', fakeAsync(() => {
expect(multiFixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1']);
- const multiCleaner = multiFixture.debugElement.query(By.css('.kbq-select__cleaner')).nativeElement;
+ const multiCleaner = multiFixture.debugElement.query(By.directive(KbqCleaner)).nativeElement;
multiCleaner.click();
multiFixture.detectChanges();
@@ -3483,7 +3519,7 @@ describe('KbqSelect', () => {
multiFixture.detectChanges();
flush();
- multiFixture.debugElement.query(By.css('.kbq-select__cleaner')).nativeElement.click();
+ multiFixture.debugElement.query(By.directive(KbqCleaner)).nativeElement.click();
multiFixture.detectChanges();
flush();
diff --git a/packages/components/select/select.component.ts b/packages/components/select/select.component.ts
index ec3ff0f389..986c72bbfc 100644
--- a/packages/components/select/select.component.ts
+++ b/packages/components/select/select.component.ts
@@ -101,7 +101,12 @@ import {
shouldSelectSearchText,
toggleSelectAll
} from '@koobiq/components/core';
-import { KbqCleaner, KbqFormField, KbqFormFieldControl } from '@koobiq/components/form-field';
+import {
+ KBQ_FORM_FIELD,
+ KbqCleaner,
+ KbqFormFieldControl,
+ kbqCleanerFactoryProvider
+} from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTag } from '@koobiq/components/tags';
import { SizeXxs as SelectSizeMultipleContentGap } from '@koobiq/design-tokens';
@@ -198,6 +203,20 @@ export const minimumTimeToDisplayLoading = 300;
styleUrls: ['./select.scss', './select-tokens.scss'],
providers: [
{ provide: KbqFormFieldControl, useExisting: KbqSelect },
+ kbqCleanerFactoryProvider(() => {
+ const select = inject(KbqSelect);
+
+ return {
+ get control() {
+ return select;
+ },
+ get keydownTarget() {
+ return select.elementRef.nativeElement;
+ },
+ clearByEscape: false,
+ clear: () => select.clear()
+ };
+ }),
{ provide: KBQ_OPTION_PARENT_COMPONENT, useExisting: KbqSelect },
{ provide: KBQ_PARENT_POPUP, useExisting: KbqSelect },
kbqSiblingPopupProvider(KbqSelect)
@@ -247,7 +266,7 @@ export class KbqSelect
private readonly _dir = inject(Directionality, { optional: true });
parentForm = inject(NgForm, { optional: true });
parentFormGroup = inject(FormGroupDirective, { optional: true });
- private readonly parentFormField = inject(KbqFormField, { host: true, optional: true })!;
+ private readonly parentFormField = inject(KBQ_FORM_FIELD, { host: true, optional: true })!;
ngControl = inject(NgControl, { self: true, optional: true });
private readonly scrollStrategyFactory = inject(KBQ_SELECT_SCROLL_STRATEGY);
protected localeService? = inject
(KBQ_LOCALE_SERVICE, { optional: true });
@@ -372,8 +391,11 @@ export class KbqSelect
/** Custom template reference for rendering tag content. */
readonly customTagTemplateRef = contentChild('kbqSelectTagContent', { read: TemplateRef });
- /** Reference to the optional cleaner element for clearing selection. */
- readonly cleaner = contentChild('kbqSelectCleaner');
+ /**
+ * Reference to the optional cleaner element for clearing selection.
+ * @docs-private
+ */
+ readonly cleaner = contentChild(KbqCleaner, { descendants: false });
/** All of the defined select options. */
@ContentChildren(KbqOption, { descendants: true }) options: QueryList;
@@ -840,9 +862,12 @@ export class KbqSelect
return !!search && this.options?.filter((option) => option.selectable()).length === 0 && !!search.value();
}
- /** Whether the cleaner (clear button) should be shown. */
+ /**
+ * Whether the cleaner (clear button) should be shown.
+ * @docs-private
+ */
get canShowCleaner(): boolean {
- return !this.disabled && !!this.cleaner() && this.selectionModel.hasValue();
+ return !!this.cleaner()?.canShow;
}
/** Returns the currently selected option(s). Single value or array for multiple selection. */
@@ -1065,20 +1090,31 @@ export class KbqSelect
/**
* Clears the current selection.
- * @param $event Mouse event to prevent default behavior.
+ * @docs-private
*/
- clearValue($event): void {
- // need to prevent opening
- $event.stopPropagation();
- // need to prevent scrolling
- $event.preventDefault();
-
+ clear(): void {
this.selectionModel.clear();
this.keyManager.setActiveItem(-1);
this.propagateChanges();
+ }
- this.focus();
+ /**
+ * Clears the current selection.
+ * @deprecated Activate the projected `KbqCleaner` instead.
+ * @docs-private
+ */
+ clearValue(event: Event): void {
+ const cleaner = this.cleaner();
+
+ if (cleaner) {
+ cleaner.clear(event);
+ } else {
+ event.stopPropagation();
+ event.preventDefault();
+ this.clear();
+ this.focus();
+ }
}
/** `View -> model callback called when value changes` */
diff --git a/packages/components/select/select.html b/packages/components/select/select.html
index cbee46cd01..df15b2339a 100644
--- a/packages/components/select/select.html
+++ b/packages/components/select/select.html
@@ -71,12 +71,7 @@
}
@if (canShowCleaner) {
-
+
}
diff --git a/packages/components/tags/e2e.ts b/packages/components/tags/e2e.ts
index 1db6587378..577457ffd5 100644
--- a/packages/components/tags/e2e.ts
+++ b/packages/components/tags/e2e.ts
@@ -204,7 +204,7 @@ export class E2eTagListStates {}
[kbqTagInputFor]="tagList"
/>
-
+
Label
@@ -222,7 +222,7 @@ export class E2eTagListStates {}
-
+
@@ -239,7 +239,7 @@ export class E2eTagListStates {}
-
+
@@ -256,7 +256,7 @@ export class E2eTagListStates {}
-
+
`,
@@ -297,7 +297,7 @@ export class E2eTagInputStates {}
[kbqAutocomplete]="autocomplete"
/>
-
+
Label
@@ -327,7 +327,7 @@ export class E2eTagInputStates {}
[kbqAutocomplete]="autocomplete2"
/>
-
+
diff --git a/packages/components/tags/tag-list.component.ts b/packages/components/tags/tag-list.component.ts
index bf8e21b6ff..c948cded78 100644
--- a/packages/components/tags/tag-list.component.ts
+++ b/packages/components/tags/tag-list.component.ts
@@ -27,7 +27,7 @@ import {
import { outputToObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ControlValueAccessor, FormGroupDirective, NgControl, NgForm, UntypedFormControl } from '@angular/forms';
import { CanUpdateErrorState, ErrorStateMatcher, FocusKeyManager, isNull, isSelectAll } from '@koobiq/components/core';
-import { KbqCleaner, KbqFormFieldControl } from '@koobiq/components/form-field';
+import { KBQ_CLEANER_CONTEXT, KbqCleaner, KbqFormFieldControl } from '@koobiq/components/form-field';
import { merge, Observable, Subject } from 'rxjs';
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { KbqTagTextControl } from './tag-text-control';
@@ -69,7 +69,11 @@ export type KbqTagListDroppedEvent = Pick, 'event' | 'previ
}
`,
styleUrls: ['tag-list.scss', 'tag-tokens.scss'],
- providers: [{ provide: KbqFormFieldControl, useExisting: KbqTagList }],
+ providers: [
+ { provide: KbqFormFieldControl, useExisting: KbqTagList },
+ // Tag-list cleaners use consumer-provided click handlers; do not also reset the outer form control.
+ { provide: KBQ_CLEANER_CONTEXT, useValue: null }
+ ],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
@@ -387,7 +391,7 @@ export class KbqTagList
readonly change = output();
/** @docs-private */
- readonly cleaner = contentChild('kbqTagListCleaner');
+ readonly cleaner = contentChild(KbqCleaner, { descendants: false });
/**
* The tag components contained within this tag list.
diff --git a/packages/components/timezone/e2e.ts b/packages/components/timezone/e2e.ts
index 10d1351c19..da41297100 100644
--- a/packages/components/timezone/e2e.ts
+++ b/packages/components/timezone/e2e.ts
@@ -65,7 +65,7 @@ class BaseTimezoneStates {
}
-
+
@@ -195,7 +195,7 @@ export class E2eTimezonePanelStates extends BaseTimezoneStates {}
[placeholder]="'Город или часовой пояс'"
[(ngModel)]="control"
/>
-
+
Ничего не найдено
diff --git a/packages/components/timezone/timezone-select.component.html b/packages/components/timezone/timezone-select.component.html
index 6d81d0575d..90d387d128 100644
--- a/packages/components/timezone/timezone-select.component.html
+++ b/packages/components/timezone/timezone-select.component.html
@@ -25,7 +25,7 @@
}
@if (canShowCleaner) {
-
+
}
diff --git a/packages/components/timezone/timezone-select.component.ts b/packages/components/timezone/timezone-select.component.ts
index b9fd3ef00e..b4e070704f 100644
--- a/packages/components/timezone/timezone-select.component.ts
+++ b/packages/components/timezone/timezone-select.component.ts
@@ -4,13 +4,14 @@ import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
+ contentChild,
Directive,
- ViewEncapsulation,
- contentChild
+ inject,
+ ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { KBQ_OPTION_PARENT_COMPONENT, kbqSiblingPopupProvider, ruRULocaleData } from '@koobiq/components/core';
-import { KbqFormFieldControl } from '@koobiq/components/form-field';
+import { kbqCleanerFactoryProvider, KbqFormFieldControl } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqSelect } from '@koobiq/components/select';
@@ -38,6 +39,20 @@ const defaultSearchPlaceholder = ruRULocaleData.timezone.searchPlaceholder;
],
providers: [
{ provide: KbqFormFieldControl, useExisting: KbqTimezoneSelect },
+ kbqCleanerFactoryProvider(() => {
+ const timezoneSelect = inject(KbqTimezoneSelect);
+
+ return {
+ get control() {
+ return timezoneSelect;
+ },
+ get keydownTarget() {
+ return timezoneSelect.elementRef.nativeElement;
+ },
+ clearByEscape: false,
+ clear: () => timezoneSelect.clear()
+ };
+ }),
{ provide: KBQ_OPTION_PARENT_COMPONENT, useExisting: KbqTimezoneSelect },
// Declared again rather than inherited from `KbqSelect`: Angular copies `providers` to a subclass only
// when that subclass has no decorator of its own.
diff --git a/packages/components/tree-select/e2e.ts b/packages/components/tree-select/e2e.ts
index 02c5c176e7..e0419d6557 100644
--- a/packages/components/tree-select/e2e.ts
+++ b/packages/components/tree-select/e2e.ts
@@ -177,7 +177,7 @@ class BaseTreeSelectStates {
Nothing found
-
+
@@ -241,7 +241,7 @@ export class E2eTreeSelectStates extends BaseTreeSelectStates {}
Nothing found
-
+
@@ -302,7 +302,7 @@ export class E2eMultiTreeSelectStates extends BaseTreeSelectStates {}
Nothing found
-
+
diff --git a/packages/components/tree-select/tree-select.component.spec.ts b/packages/components/tree-select/tree-select.component.spec.ts
index 969b1344ff..3a53aeafe5 100644
--- a/packages/components/tree-select/tree-select.component.spec.ts
+++ b/packages/components/tree-select/tree-select.component.spec.ts
@@ -62,7 +62,7 @@ import {
kbqErrorStateMatcherProvider,
wrappedErrorMessage
} from '@koobiq/components/core';
-import { KbqFormFieldModule } from '@koobiq/components/form-field';
+import { KbqCleaner, KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqSelectModule } from '@koobiq/components/select';
@@ -326,7 +326,7 @@ const getChildren = (node: FileNode): Observable => {
-
+
@@ -2865,7 +2865,7 @@ describe('KbqTreeSelect', () => {
expect(value.nativeElement.textContent).toContain('rootNode_1');
- cleaner = fixture.debugElement.query(By.css('.kbq-select__cleaner')).nativeElement;
+ cleaner = fixture.debugElement.query(By.directive(KbqCleaner)).nativeElement;
cleaner.click();
fixture.detectChanges();
tick();
diff --git a/packages/components/tree-select/tree-select.component.ts b/packages/components/tree-select/tree-select.component.ts
index 7235cea36c..b8ae8e9ea8 100644
--- a/packages/components/tree-select/tree-select.component.ts
+++ b/packages/components/tree-select/tree-select.component.ts
@@ -87,7 +87,12 @@ import {
kbqSiblingPopupProvider,
shouldSelectSearchText
} from '@koobiq/components/core';
-import { KbqCleaner, KbqFormField, KbqFormFieldControl } from '@koobiq/components/form-field';
+import {
+ KBQ_FORM_FIELD,
+ KbqCleaner,
+ KbqFormFieldControl,
+ kbqCleanerFactoryProvider
+} from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTag, KbqTagRemove } from '@koobiq/components/tags';
import { KbqTree, KbqTreeOption, KbqTreeSelection } from '@koobiq/components/tree';
@@ -177,6 +182,20 @@ export class KbqTreeSelectChange {
styleUrls: ['./tree-select.scss', './tree-select-tokens.scss', '../select/select-tokens.scss'],
providers: [
{ provide: KbqFormFieldControl, useExisting: KbqTreeSelect },
+ kbqCleanerFactoryProvider(() => {
+ const treeSelect = inject(KbqTreeSelect);
+
+ return {
+ get control() {
+ return treeSelect;
+ },
+ get keydownTarget() {
+ return treeSelect.elementRef.nativeElement;
+ },
+ clearByEscape: false,
+ clear: () => treeSelect.clear()
+ };
+ }),
{ provide: KbqTree, useExisting: KbqTreeSelect },
{ provide: KBQ_PARENT_POPUP, useExisting: KbqTreeSelect },
kbqSiblingPopupProvider(KbqTreeSelect)
@@ -224,7 +243,7 @@ export class KbqTreeSelect
private readonly dir = inject(Directionality, { optional: true });
parentForm = inject(NgForm, { optional: true });
parentFormGroup = inject(FormGroupDirective, { optional: true });
- private readonly parentFormField = inject(KbqFormField, { host: true, optional: true })!;
+ private readonly parentFormField = inject(KBQ_FORM_FIELD, { host: true, optional: true })!;
ngControl = inject(NgControl, { optional: true, self: true });
private localeService = inject(KBQ_LOCALE_SERVICE, { optional: true });
protected readonly isBrowser = inject(Platform).isBrowser;
@@ -316,7 +335,11 @@ export class KbqTreeSelect
@ViewChildren(KbqTag) tags: QueryList;
- readonly cleaner = contentChild('kbqSelectCleaner');
+ /**
+ * Reference to the optional cleaner element for clearing selection.
+ * @docs-private
+ */
+ readonly cleaner = contentChild(KbqCleaner, { descendants: false });
/** User-supplied override of the trigger element. */
readonly customTrigger = contentChild(KbqSelectTrigger);
@@ -709,8 +732,12 @@ export class KbqTreeSelect
return this._panelOpen;
}
+ /**
+ * Whether the cleaner (clear button) should be shown.
+ * @docs-private
+ */
get canShowCleaner(): boolean {
- return !this.disabled && !!this.cleaner() && this.selectionModel.hasValue();
+ return !!this.cleaner()?.canShow;
}
/** @docs-private */
@@ -920,22 +947,34 @@ export class KbqTreeSelect
return hiddenItemsText.replace('{{ number }}', hiddenItems.toString());
}
- clearValue($event): void {
- // need to prevent opening
- $event.stopPropagation();
- // need to prevent scrolling
- $event.preventDefault();
-
+ /**
+ * Clears the current selection.
+ * @docs-private
+ */
+ clear(): void {
this.selectionModel.clear();
this.tree()!.keyManager.setActiveItem(-1);
-
this.setSelectionByValue([]);
-
this.onChange(this.selectedValues);
-
this.selectionChange.emit(new KbqTreeSelectChange(this, this.selectedValues));
+ }
- this.focus();
+ /**
+ * Clears the current selection.
+ * @deprecated Activate the projected `KbqCleaner` instead.
+ * @docs-private
+ */
+ clearValue(event: Event): void {
+ const cleaner = this.cleaner();
+
+ if (cleaner) {
+ cleaner.clear(event);
+ } else {
+ event.stopPropagation();
+ event.preventDefault();
+ this.clear();
+ this.focus();
+ }
}
/** `View -> model callback called when value changes` */
diff --git a/packages/components/tree-select/tree-select.html b/packages/components/tree-select/tree-select.html
index 291aecc077..ad725a2491 100644
--- a/packages/components/tree-select/tree-select.html
+++ b/packages/components/tree-select/tree-select.html
@@ -70,12 +70,7 @@
}
@if (canShowCleaner) {
-
+
}
diff --git a/packages/components/tree/tree-selection.component.ts b/packages/components/tree/tree-selection.component.ts
index 6fa82e00e6..545cc03d45 100644
--- a/packages/components/tree/tree-selection.component.ts
+++ b/packages/components/tree/tree-selection.component.ts
@@ -39,7 +39,6 @@ import {
isCopy,
isSelectAll,
isVerticalMovement,
- KBQ_FORM_FIELD_REF,
LEFT_ARROW,
MultipleMode,
PAGE_DOWN,
@@ -50,6 +49,7 @@ import {
toggleSelectAll,
UP_ARROW
} from '@koobiq/components/core';
+import { KBQ_FORM_FIELD } from '@koobiq/components/form-field';
import { merge, Observable, Subscription } from 'rxjs';
import { AsyncScheduler } from 'rxjs/internal/scheduler/AsyncScheduler';
import { delay } from 'rxjs/operators';
@@ -148,7 +148,7 @@ export class KbqTreeSelection
* by the host `KbqTreeSelect` so hover works even when the tree-select has no form-field
* wrapper (e.g. the filter-bar pipes render the tree-select bare).
*/
- inSelect = !!inject(KBQ_FORM_FIELD_REF, { optional: true, host: true });
+ inSelect = !!inject(KBQ_FORM_FIELD, { optional: true, host: true });
renderedOptions = new QueryList
();
diff --git a/packages/docs-examples/components/ag-grid/ag-grid-external-filter-state/ag-grid-external-filter-state-example.ts b/packages/docs-examples/components/ag-grid/ag-grid-external-filter-state/ag-grid-external-filter-state-example.ts
index 72ab9534f8..1b6637742e 100644
--- a/packages/docs-examples/components/ag-grid/ag-grid-external-filter-state/ag-grid-external-filter-state-example.ts
+++ b/packages/docs-examples/components/ag-grid/ag-grid-external-filter-state/ag-grid-external-filter-state-example.ts
@@ -21,7 +21,7 @@ ModuleRegistry.registerModules([AllCommunityModule]);
template: `
-
+
@for (option of options; track option) {
{{ option }}
}
diff --git a/packages/docs-examples/components/form-field/form-field-with-cleaner/form-field-with-cleaner-example.ts b/packages/docs-examples/components/form-field/form-field-with-cleaner/form-field-with-cleaner-example.ts
index 27f5b92766..2e62d0b89c 100644
--- a/packages/docs-examples/components/form-field/form-field-with-cleaner/form-field-with-cleaner-example.ts
+++ b/packages/docs-examples/components/form-field/form-field-with-cleaner/form-field-with-cleaner-example.ts
@@ -15,7 +15,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
-
+
Option #1
Option #2
diff --git a/packages/docs-examples/components/inline-edit/inline-edit-controls/inline-edit-controls-example.ts b/packages/docs-examples/components/inline-edit/inline-edit-controls/inline-edit-controls-example.ts
index 840caecd80..1a60f51887 100644
--- a/packages/docs-examples/components/inline-edit/inline-edit-controls/inline-edit-controls-example.ts
+++ b/packages/docs-examples/components/inline-edit/inline-edit-controls/inline-edit-controls-example.ts
@@ -93,7 +93,7 @@ import { KbqTextareaModule } from '@koobiq/components/textarea';
@for (option of options; track option) {
{{ option }}
}
-
+
diff --git a/packages/docs-examples/components/inline-edit/inline-edit-overview/inline-edit-overview-example.ts b/packages/docs-examples/components/inline-edit/inline-edit-overview/inline-edit-overview-example.ts
index 5c2c107cdc..6587985e8a 100644
--- a/packages/docs-examples/components/inline-edit/inline-edit-overview/inline-edit-overview-example.ts
+++ b/packages/docs-examples/components/inline-edit/inline-edit-overview/inline-edit-overview-example.ts
@@ -35,7 +35,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
@for (option of options; track option) {
{{ option }}
}
-
+
diff --git a/packages/docs-examples/components/select/select-cleaner/select-cleaner-example.ts b/packages/docs-examples/components/select/select-cleaner/select-cleaner-example.ts
index abc5216cdc..45a0b727dd 100644
--- a/packages/docs-examples/components/select/select-cleaner/select-cleaner-example.ts
+++ b/packages/docs-examples/components/select/select-cleaner/select-cleaner-example.ts
@@ -10,7 +10,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
template: `
-
+
@for (option of options; track option) {
{{ option }}
}
diff --git a/packages/docs-examples/components/select/select-multiple/select-multiple-example.ts b/packages/docs-examples/components/select/select-multiple/select-multiple-example.ts
index fc8f17451a..d8f0d0d333 100644
--- a/packages/docs-examples/components/select/select-multiple/select-multiple-example.ts
+++ b/packages/docs-examples/components/select/select-multiple/select-multiple-example.ts
@@ -14,7 +14,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
{{ option }}
}
-
+
`,
diff --git a/packages/docs-examples/components/select/select-preselected-values/select-preselected-values-example.ts b/packages/docs-examples/components/select/select-preselected-values/select-preselected-values-example.ts
index 0df7062309..e2df8f4fb1 100644
--- a/packages/docs-examples/components/select/select-preselected-values/select-preselected-values-example.ts
+++ b/packages/docs-examples/components/select/select-preselected-values/select-preselected-values-example.ts
@@ -15,7 +15,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
{{ option }}
}
-
+
`,
diff --git a/packages/docs-examples/components/select/select-prioritized-selected/select-prioritized-selected-example.ts b/packages/docs-examples/components/select/select-prioritized-selected/select-prioritized-selected-example.ts
index f7923141b3..39a9931d70 100644
--- a/packages/docs-examples/components/select/select-prioritized-selected/select-prioritized-selected-example.ts
+++ b/packages/docs-examples/components/select/select-prioritized-selected/select-prioritized-selected-example.ts
@@ -14,7 +14,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
{{ option }}
}
-
+
`,
diff --git a/packages/docs-examples/components/select/select-with-multiline-matcher/select-with-multiline-matcher-example.ts b/packages/docs-examples/components/select/select-with-multiline-matcher/select-with-multiline-matcher-example.ts
index 7df07619c7..0ba5231d1b 100644
--- a/packages/docs-examples/components/select/select-with-multiline-matcher/select-with-multiline-matcher-example.ts
+++ b/packages/docs-examples/components/select/select-with-multiline-matcher/select-with-multiline-matcher-example.ts
@@ -16,7 +16,7 @@ import { KbqSelectModule } from '@koobiq/components/select';
@for (option of options; track option) {
{{ option }}
}
-
+
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-draggable/tag-autocomplete-draggable-example.ts b/packages/docs-examples/components/tags/tag-autocomplete-draggable/tag-autocomplete-draggable-example.ts
index 5286809310..e39231b4af 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-draggable/tag-autocomplete-draggable-example.ts
+++ b/packages/docs-examples/components/tags/tag-autocomplete-draggable/tag-autocomplete-draggable-example.ts
@@ -62,7 +62,7 @@ const getAutocompleteOptions = () => [
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-editable/tag-autocomplete-editable-example.ts b/packages/docs-examples/components/tags/tag-autocomplete-editable/tag-autocomplete-editable-example.ts
index 7cd63f7991..68c3d621f5 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-editable/tag-autocomplete-editable-example.ts
+++ b/packages/docs-examples/components/tags/tag-autocomplete-editable/tag-autocomplete-editable-example.ts
@@ -68,7 +68,7 @@ const getAutocompleteOptions = () => [
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-onpaste-off/tag-autocomplete-onpaste-off-example.html b/packages/docs-examples/components/tags/tag-autocomplete-onpaste-off/tag-autocomplete-onpaste-off-example.html
index 9cf53e7993..e94c1747d8 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-onpaste-off/tag-autocomplete-onpaste-off-example.html
+++ b/packages/docs-examples/components/tags/tag-autocomplete-onpaste-off/tag-autocomplete-onpaste-off-example.html
@@ -23,7 +23,7 @@
(kbqTagInputTokenEnd)="onCreate($event)"
/>
-
+
@if (canCreate) {
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-option-operations/tag-autocomplete-option-operations-example.ts b/packages/docs-examples/components/tags/tag-autocomplete-option-operations/tag-autocomplete-option-operations-example.ts
index 8063d9dd4b..344a741ed3 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-option-operations/tag-autocomplete-option-operations-example.ts
+++ b/packages/docs-examples/components/tags/tag-autocomplete-option-operations/tag-autocomplete-option-operations-example.ts
@@ -62,7 +62,7 @@ const getAutocompleteOptions = () => [
(kbqTagInputTokenEnd)="onCreate($event)"
/>
-
+
@if (canCreate) {
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-overview/tag-autocomplete-overview-example.ts b/packages/docs-examples/components/tags/tag-autocomplete-overview/tag-autocomplete-overview-example.ts
index 8265adaf92..48d948a855 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-overview/tag-autocomplete-overview-example.ts
+++ b/packages/docs-examples/components/tags/tag-autocomplete-overview/tag-autocomplete-overview-example.ts
@@ -62,7 +62,7 @@ const getAutocompleteOptions = () => [
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
diff --git a/packages/docs-examples/components/tags/tag-autocomplete-removable/tag-autocomplete-removable-example.ts b/packages/docs-examples/components/tags/tag-autocomplete-removable/tag-autocomplete-removable-example.ts
index 7fb05ec13e..b9e2517667 100644
--- a/packages/docs-examples/components/tags/tag-autocomplete-removable/tag-autocomplete-removable-example.ts
+++ b/packages/docs-examples/components/tags/tag-autocomplete-removable/tag-autocomplete-removable-example.ts
@@ -55,7 +55,7 @@ const getAutocompleteOptions = () => [
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
diff --git a/packages/docs-examples/components/tags/tag-input-draggable/tag-input-draggable-example.ts b/packages/docs-examples/components/tags/tag-input-draggable/tag-input-draggable-example.ts
index ebd3ae8658..44250a73db 100644
--- a/packages/docs-examples/components/tags/tag-input-draggable/tag-input-draggable-example.ts
+++ b/packages/docs-examples/components/tags/tag-input-draggable/tag-input-draggable-example.ts
@@ -38,7 +38,7 @@ const getTags = () => Array.from({ length: 3 }, (_, id) => ({ id, value: `Dragga
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
`,
diff --git a/packages/docs-examples/components/tags/tag-input-editable/tag-input-editable-example.ts b/packages/docs-examples/components/tags/tag-input-editable/tag-input-editable-example.ts
index 12fdaec489..da757726c5 100644
--- a/packages/docs-examples/components/tags/tag-input-editable/tag-input-editable-example.ts
+++ b/packages/docs-examples/components/tags/tag-input-editable/tag-input-editable-example.ts
@@ -45,7 +45,7 @@ const getTags = () => Array.from({ length: 3 }, (_, i) => ({ value: `Editable ta
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
`,
diff --git a/packages/docs-examples/components/tags/tag-input-onpaste-off/tag-input-onpaste-off-example.html b/packages/docs-examples/components/tags/tag-input-onpaste-off/tag-input-onpaste-off-example.html
index 0324752f4a..c81fd3c57b 100644
--- a/packages/docs-examples/components/tags/tag-input-onpaste-off/tag-input-onpaste-off-example.html
+++ b/packages/docs-examples/components/tags/tag-input-onpaste-off/tag-input-onpaste-off-example.html
@@ -17,6 +17,6 @@
[kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
(kbqTagInputTokenEnd)="onCreate($event)"
/>
-
+
diff --git a/packages/docs-examples/components/tags/tag-input-overview/tag-input-overview-example.ts b/packages/docs-examples/components/tags/tag-input-overview/tag-input-overview-example.ts
index 22467ab628..f2f029023b 100644
--- a/packages/docs-examples/components/tags/tag-input-overview/tag-input-overview-example.ts
+++ b/packages/docs-examples/components/tags/tag-input-overview/tag-input-overview-example.ts
@@ -39,7 +39,7 @@ const getTags = () => Array.from({ length: 3 }, (_, i) => ({ value: `Tag ${i}` }
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
`,
diff --git a/packages/docs-examples/components/tags/tag-input-removable/tag-input-removable-example.ts b/packages/docs-examples/components/tags/tag-input-removable/tag-input-removable-example.ts
index e5396e4647..5091548276 100644
--- a/packages/docs-examples/components/tags/tag-input-removable/tag-input-removable-example.ts
+++ b/packages/docs-examples/components/tags/tag-input-removable/tag-input-removable-example.ts
@@ -32,7 +32,7 @@ const getTags = () => Array.from({ length: 3 }, (_, i) => ({ value: `Removable t
(kbqTagInputTokenEnd)="create($event)"
/>
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-child-selection-overview/tree-select-child-selection-overview-example.ts b/packages/docs-examples/components/tree-select/tree-select-child-selection-overview/tree-select-child-selection-overview-example.ts
index 7fcf660648..36cafc5bb3 100644
--- a/packages/docs-examples/components/tree-select/tree-select-child-selection-overview/tree-select-child-selection-overview-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-child-selection-overview/tree-select-child-selection-overview-example.ts
@@ -131,7 +131,7 @@ export const DATA_OBJECT = {
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-footer-overview/tree-select-footer-overview-example.ts b/packages/docs-examples/components/tree-select/tree-select-footer-overview/tree-select-footer-overview-example.ts
index 591dc14677..97fbfa42c0 100644
--- a/packages/docs-examples/components/tree-select/tree-select-footer-overview/tree-select-footer-overview-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-footer-overview/tree-select-footer-overview-example.ts
@@ -121,7 +121,7 @@ export const DATA_OBJECT = {
-
+
extends KbqTreeFlatDataSource {
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-multiple-overview/tree-select-multiple-overview-example.ts b/packages/docs-examples/components/tree-select/tree-select-multiple-overview/tree-select-multiple-overview-example.ts
index 570922c891..78ecf38ccf 100644
--- a/packages/docs-examples/components/tree-select/tree-select-multiple-overview/tree-select-multiple-overview-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-multiple-overview/tree-select-multiple-overview-example.ts
@@ -130,7 +130,7 @@ export const DATA_OBJECT = {
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-overview/tree-select-overview-example.ts b/packages/docs-examples/components/tree-select/tree-select-overview/tree-select-overview-example.ts
index 82c7344c14..0c209c9948 100644
--- a/packages/docs-examples/components/tree-select/tree-select-overview/tree-select-overview-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-overview/tree-select-overview-example.ts
@@ -120,7 +120,7 @@ export const DATA_OBJECT = {
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-search-overview/tree-select-search-overview-example.ts b/packages/docs-examples/components/tree-select/tree-select-search-overview/tree-select-search-overview-example.ts
index 163409f3a3..cd49f6a423 100644
--- a/packages/docs-examples/components/tree-select/tree-select-search-overview/tree-select-search-overview-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-search-overview/tree-select-search-overview-example.ts
@@ -146,7 +146,7 @@ export const DATA_OBJECT = {
-
+
`,
diff --git a/packages/docs-examples/components/tree-select/tree-select-with-multiline-matcher-overview/tree-select-with-multiline-matcher-example.ts b/packages/docs-examples/components/tree-select/tree-select-with-multiline-matcher-overview/tree-select-with-multiline-matcher-example.ts
index 5b0a613bba..32c9144a13 100644
--- a/packages/docs-examples/components/tree-select/tree-select-with-multiline-matcher-overview/tree-select-with-multiline-matcher-example.ts
+++ b/packages/docs-examples/components/tree-select/tree-select-with-multiline-matcher-overview/tree-select-with-multiline-matcher-example.ts
@@ -120,7 +120,7 @@ export const DATA_OBJECT = {
-
+
`,
diff --git a/packages/e2e/routes.ts b/packages/e2e/routes.ts
index 36320faae6..69fea1e671 100644
--- a/packages/e2e/routes.ts
+++ b/packages/e2e/routes.ts
@@ -41,7 +41,7 @@ import {
E2eFilterBarStates
} from '../components/filter-bar/e2e';
import { E2eFlagStyles } from '../components/flag/e2e';
-import { E2eFormFieldGroup, E2eFormFieldset } from '../components/form-field/e2e';
+import { E2eFormFieldAddons, E2eFormFieldGroup, E2eFormFieldset } from '../components/form-field/e2e';
import { E2eIconStateAndStyle, E2eIconSvg } from '../components/icon/e2e';
import {
E2eInlineEditActionButtons,
@@ -145,6 +145,7 @@ const components = [
E2eSplitterGhost,
E2eFileUploadStateAndStyle,
E2eFileUploadDropzone,
+ E2eFormFieldAddons,
E2eFormFieldGroup,
E2eFormFieldset,
E2eActionsPanelWithOverlayContainer,
diff --git a/tools/public_api_guard/components/core.api.md b/tools/public_api_guard/components/core.api.md
index 4bf59f8a4e..bec0b7e218 100644
--- a/tools/public_api_guard/components/core.api.md
+++ b/tools/public_api_guard/components/core.api.md
@@ -2356,7 +2356,7 @@ export const KBQ_DEFAULT_LOCALE_ID = "ru-RU";
// @public
export const KBQ_DEFAULT_PRECISION_SEPARATOR = ".";
-// @public
+// @public @deprecated
export const KBQ_FORM_FIELD_REF: InjectionToken;
// @public (undocumented)
@@ -2869,7 +2869,7 @@ export interface KbqFormFieldControlRef {
value: T | null;
}
-// @public
+// @public @deprecated
export interface KbqFormFieldRef {
// (undocumented)
canCleanerClearByEsc: boolean;
diff --git a/tools/public_api_guard/components/datepicker.api.md b/tools/public_api_guard/components/datepicker.api.md
index 9ff6d469b5..96709b0e76 100644
--- a/tools/public_api_guard/components/datepicker.api.md
+++ b/tools/public_api_guard/components/datepicker.api.md
@@ -16,7 +16,6 @@ import { ElementRef } from '@angular/core';
import { ErrorStateMatcher } from '@koobiq/components/core';
import { EventEmitter } from '@angular/core';
import * as i1 from '@koobiq/components/button';
-import * as i15 from '@koobiq/components/form-field';
import * as i2 from '@angular/cdk/overlay';
import * as i3 from '@angular/cdk/a11y';
import * as i4 from '@angular/cdk/portal';
@@ -24,11 +23,11 @@ import * as i5 from '@koobiq/components/select';
import * as i6 from '@koobiq/components/icon';
import * as i8 from '@angular/common';
import { InjectionToken } from '@angular/core';
-import { KbqFormField } from '@koobiq/components/form-field';
import { KbqFormFieldControl } from '@koobiq/components/form-field';
import { KbqSiblingPopup } from '@koobiq/components/core';
import { KbqTooltipTrigger } from '@koobiq/components/tooltip';
import * as _koobiq_components_core from '@koobiq/components/core';
+import * as _koobiq_components_form_field from '@koobiq/components/form-field';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
@@ -311,7 +310,7 @@ export class KbqDatepickerInput implements KbqFormFieldControl, ControlVal
focusChanged(isFocused: boolean): void;
// (undocumented)
focused: boolean;
- protected readonly formField: KbqFormField | null;
+ protected readonly formField: _koobiq_components_form_field.KbqFormField | null;
getOrigin(): ElementRef;
// (undocumented)
get id(): string;
@@ -421,7 +420,7 @@ export class KbqDatepickerModule {
// (undocumented)
static ɵinj: _angular_core.ɵɵInjectorDeclaration;
// (undocumented)
- static ɵmod: _angular_core.ɵɵNgModuleDeclaration;
+ static ɵmod: _angular_core.ɵɵNgModuleDeclaration;
}
// @public
diff --git a/tools/public_api_guard/components/form-field.api.md b/tools/public_api_guard/components/form-field.api.md
index 0062d2abaf..6094eaf5eb 100644
--- a/tools/public_api_guard/components/form-field.api.md
+++ b/tools/public_api_guard/components/form-field.api.md
@@ -36,6 +36,12 @@ export function getKbqFormFieldYouCanNotUseCleanerInNumberInputError(): Error;
// @public @deprecated
export const hasPasswordStrengthError: (passwordHints: QueryList | readonly KbqPasswordHint[]) => boolean;
+// @public
+export const KBQ_CLEANER_CONTEXT: InjectionToken;
+
+// @public
+export const KBQ_FORM_FIELD: InjectionToken;
+
// @public
export const KBQ_FORM_FIELD_DEFAULT_OPTIONS: InjectionToken;
readonly ariaLabel: i0.InputSignal;
+ get canShow(): boolean;
+ clear(event: Event): void;
+ // (undocumented)
+ ngAfterContentInit(): void;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
+// @public
+export interface KbqCleanerContext {
+ clear?(): void;
+ // (undocumented)
+ readonly clearByEscape: boolean;
+ // (undocumented)
+ readonly control: KbqFormFieldControl;
+ // (undocumented)
+ readonly keydownTarget: HTMLElement;
+}
+
+// @public
+export const kbqCleanerFactoryProvider: (factory: () => KbqCleanerContext) => Provider;
+
// @public
export class KbqError extends KbqHint {
constructor();
@@ -90,6 +114,7 @@ export class KbqFieldsetItem {
// @public
export class KbqFormField extends KbqColorDirective implements AfterContentInit, AfterViewInit, OnDestroy, AfterContentChecked {
+ // @deprecated
canCleanerClearByEsc: boolean;
get canShowCleaner(): boolean;
readonly cleaner: Signal;
@@ -130,7 +155,8 @@ export class KbqFormField extends KbqColorDirective implements AfterContentInit,
readonly noBorders: i0.InputSignalWithTransform;
onContainerClick(event: MouseEvent): void;
onHoverChanged(isHovered: boolean): void;
- onKeyDown(event: KeyboardEvent): void;
+ // @deprecated
+ onKeyDown(_event: KeyboardEvent): void;
readonly passwordHints: Signal;
readonly passwordToggle: Signal;
readonly prefix: Signal;
@@ -215,13 +241,6 @@ export class KbqLegend {
static ɵfac: i0.ɵɵFactoryDeclaration;
}
-// @public
-export interface KbqNumberInputControl extends KbqFormFieldControl {
- step: number;
- stepDown: (step: number) => void;
- stepUp: (step: number) => void;
-}
-
// @public @deprecated
export class KbqPasswordHint extends KbqHint implements AfterContentInit {
constructor();
@@ -263,8 +282,11 @@ export class KbqPasswordToggle extends KbqTooltipTrigger implements AfterViewIni
readonly kbqTooltipHidden: i0.InputSignal>;
// (undocumented)
protected readonly nativeElement: HTMLElement;
+ // (undocumented)
ngAfterContentInit(): void;
+ // (undocumented)
ngAfterViewInit(): void;
+ // (undocumented)
ngOnDestroy(): void;
// (undocumented)
readonly tabindex: i0.InputSignalWithTransform;
@@ -302,11 +324,13 @@ export function kbqSetDescribedByIds(element: HTMLElement, ids: string[]): void;
// @public
export class KbqStepper {
constructor();
- connectTo(numberInput: KbqNumberInputControl): void;
- protected readonly control: i0.Signal;
+ // @deprecated (undocumented)
+ connectTo(_: any): void;
+ // Warning: (ae-forgotten-export) The symbol "KbqStepperControl" needs to be exported by the entry point index.d.ts
+ protected readonly control: i0.Signal;
protected readonly mouseUp: Subject;
- onStepDown($event: MouseEvent): void;
- onStepUp($event: MouseEvent): void;
+ onStepDown(event: MouseEvent): void;
+ onStepUp(event: MouseEvent): void;
readonly stepDown: OutputEmitterRef;
readonly stepUp: OutputEmitterRef;
// (undocumented)
diff --git a/tools/public_api_guard/components/icon.api.md b/tools/public_api_guard/components/icon.api.md
index 4c7ae1a780..7d8a862669 100644
--- a/tools/public_api_guard/components/icon.api.md
+++ b/tools/public_api_guard/components/icon.api.md
@@ -13,7 +13,6 @@ import { FocusMonitor } from '@angular/cdk/a11y';
import * as i0 from '@angular/core';
import { InjectionToken } from '@angular/core';
import { KbqColorDirective } from '@koobiq/components/core';
-import * as _koobiq_components_core from '@koobiq/components/core';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
@@ -23,6 +22,9 @@ import { SafeHtml } from '@angular/platform-browser';
import { SafeResourceUrl } from '@angular/platform-browser';
import { SimpleChanges } from '@angular/core';
+// @public
+export const KBQ_ICON_ERROR_STATE_CONTEXT: InjectionToken;
+
// @public
export const KBQ_ICON_RESOLVER: InjectionToken;
@@ -39,8 +41,7 @@ export class KbqIcon extends KbqColorDirective implements AfterContentInit, OnCh
protected readonly destroyRef: DestroyRef;
// (undocumented)
readonly elementRef: ElementRef;
- // (undocumented)
- protected readonly formField: _koobiq_components_core.KbqFormFieldRef | null;
+ protected readonly errorStateContext: KbqIconErrorStateContext | null;
// (undocumented)
getHostElement(): HTMLElement;
// (undocumented)
@@ -100,6 +101,15 @@ export class KbqIconButton extends KbqIcon implements AfterViewInit, OnDestroy {
// @public
export type KbqIconButtonSize = 'compact' | 'normal';
+// @public
+export interface KbqIconErrorStateContext {
+ readonly errorState: boolean;
+ readonly stateChanges: Observable;
+}
+
+// @public
+export const kbqIconErrorStateContextFactoryProvider: (factory: () => KbqIconErrorStateContext) => Provider;
+
// @public (undocumented)
export class KbqIconItem extends KbqIcon {
// (undocumented)
diff --git a/tools/public_api_guard/components/input.api.md b/tools/public_api_guard/components/input.api.md
index 3ead8c47b9..a267605958 100644
--- a/tools/public_api_guard/components/input.api.md
+++ b/tools/public_api_guard/components/input.api.md
@@ -192,7 +192,6 @@ export class KbqInputPassword implements KbqFormFieldControl, OnChanges, On
get required(): boolean;
set required(value: boolean);
readonly stateChanges: Subject;
- // (undocumented)
toggleType(): void;
// (undocumented)
protected uid: string;
@@ -211,8 +210,7 @@ export class KbqNumberInput implements KbqFormFieldControl, ControlValueAcc
constructor();
// (undocumented)
bigStep: number;
- // (undocumented)
- controlType?: string | undefined;
+ controlType: string;
// (undocumented)
get disabled(): boolean;
set disabled(value: boolean);
diff --git a/tools/public_api_guard/components/select.api.md b/tools/public_api_guard/components/select.api.md
index 9be1beeb7a..180f2d95e5 100644
--- a/tools/public_api_guard/components/select.api.md
+++ b/tools/public_api_guard/components/select.api.md
@@ -106,7 +106,9 @@ export class KbqSelect extends KbqAbstractSelect implements AfterContentInit, On
get canShowCleaner(): boolean;
readonly cdkVirtualForOf: _angular_core.Signal | undefined>;
readonly cleaner: _angular_core.Signal;
- clearValue($event: any): void;
+ clear(): void;
+ // @deprecated
+ clearValue(event: Event): void;
close(): void;
readonly closedStream: Observable;
get colorForState(): KbqComponentColors;
diff --git a/tools/public_api_guard/components/tree-select.api.md b/tools/public_api_guard/components/tree-select.api.md
index bef89764b7..9bf9b57e14 100644
--- a/tools/public_api_guard/components/tree-select.api.md
+++ b/tools/public_api_guard/components/tree-select.api.md
@@ -74,14 +74,13 @@ export class KbqTreeSelect extends KbqAbstractSelect implements AfterContentInit
readonly backdropClass: _angular_core.InputSignal;
// (undocumented)
calculateHiddenItems: () => void;
- // (undocumented)
get canShowCleaner(): boolean;
// (undocumented)
readonly changeDetectorRef: ChangeDetectorRef;
- // (undocumented)
readonly cleaner: _angular_core.Signal;
- // (undocumented)
- clearValue($event: any): void;
+ clear(): void;
+ // @deprecated
+ clearValue(event: Event): void;
close(): void;
readonly closedStream: Observable;
get colorForState(): KbqComponentColors;