From 8ad707d5ec95b165f6c762d6e71fcf081f2ec9fa Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Wed, 5 Aug 2026 14:43:58 +0300 Subject: [PATCH 1/2] feat(form-field): independent addons (#DS-3883) --- AGENTS.md | 3 +- .../components-dev/form-field/template.html | 4 +- packages/components-dev/select/template.html | 12 +- packages/components-dev/tag/template.html | 6 +- .../components-dev/tree-select/template.html | 2 +- .../autocomplete-trigger.directive.ts | 4 +- .../autocomplete/autocomplete.component.ts | 4 +- .../core/form-field/form-field-ref.ts | 6 +- .../datepicker/datepicker-input.directive.ts | 4 +- .../filter-bar/pipes/pipe-input.spec.ts | 4 +- .../components/filter-bar/pipes/pipe-input.ts | 10 +- .../pipes/pipe-multi-tree-select.spec.ts | 2 +- .../filter-bar/pipes/pipe-tree-select.html | 2 +- .../form-field/_form-field-theme.scss | 1 + packages/components/form-field/cleaner.ts | 106 +++++++++++++++++- .../form-field/e2e.playwright-spec.ts | 34 ++++++ packages/components/form-field/e2e.ts | 46 +++++++- .../components/form-field/form-field.html | 7 +- .../components/form-field/form-field.spec.ts | 43 ++++++- packages/components/form-field/form-field.ts | 98 ++++++++-------- .../components/form-field/password-toggle.ts | 80 ++++++++----- .../form-field/reactive-password-hint.ts | 7 +- packages/components/form-field/stepper.ts | 101 +++++++++-------- .../icon/icon-error-state-context.ts | 29 +++++ packages/components/icon/icon.component.ts | 13 ++- packages/components/icon/public-api.ts | 1 + .../inline-edit/inline-edit.spec.ts | 2 +- .../components/input/e2e.playwright-spec.ts | 2 + .../components/input/input-number.spec.ts | 56 ++++++++- packages/components/input/input-number.ts | 6 +- .../components/input/input-password.spec.ts | 35 +++++- packages/components/input/input-password.ts | 4 +- packages/components/input/input.spec.ts | 2 +- packages/components/input/input.ts | 2 +- .../select/select.component.spec.ts | 50 +++++++-- .../components/select/select.component.ts | 64 ++++++++--- packages/components/select/select.html | 7 +- packages/components/tags/e2e.ts | 12 +- .../components/tags/tag-list.component.ts | 10 +- packages/components/timezone/e2e.ts | 4 +- .../timezone/timezone-select.component.html | 2 +- .../timezone/timezone-select.component.ts | 21 +++- packages/components/tree-select/e2e.ts | 6 +- .../tree-select/tree-select.component.spec.ts | 6 +- .../tree-select/tree-select.component.ts | 67 ++++++++--- .../components/tree-select/tree-select.html | 7 +- .../tree/tree-selection.component.ts | 4 +- .../ag-grid-external-filter-state-example.ts | 2 +- .../form-field-with-cleaner-example.ts | 2 +- .../inline-edit-controls-example.ts | 2 +- .../inline-edit-overview-example.ts | 2 +- .../select-cleaner/select-cleaner-example.ts | 2 +- .../select-multiple-example.ts | 2 +- .../select-preselected-values-example.ts | 2 +- .../select-prioritized-selected-example.ts | 2 +- .../select-with-multiline-matcher-example.ts | 2 +- .../tag-autocomplete-draggable-example.ts | 2 +- .../tag-autocomplete-editable-example.ts | 2 +- .../tag-autocomplete-onpaste-off-example.html | 2 +- ...-autocomplete-option-operations-example.ts | 2 +- .../tag-autocomplete-overview-example.ts | 2 +- .../tag-autocomplete-removable-example.ts | 2 +- .../tag-input-draggable-example.ts | 2 +- .../tag-input-editable-example.ts | 2 +- .../tag-input-onpaste-off-example.html | 2 +- .../tag-input-overview-example.ts | 2 +- .../tag-input-removable-example.ts | 2 +- ...select-child-selection-overview-example.ts | 2 +- .../tree-select-footer-overview-example.ts | 2 +- .../tree-select-lazyload-example.ts | 2 +- .../tree-select-multiple-overview-example.ts | 2 +- .../tree-select-overview-example.ts | 2 +- .../tree-select-search-overview-example.ts | 2 +- ...e-select-with-multiline-matcher-example.ts | 2 +- packages/e2e/routes.ts | 3 +- tools/public_api_guard/components/core.api.md | 4 +- .../components/datepicker.api.md | 7 +- .../components/form-field.api.md | 50 ++++++--- tools/public_api_guard/components/icon.api.md | 16 ++- .../public_api_guard/components/input.api.md | 4 +- .../public_api_guard/components/select.api.md | 4 +- .../components/tree-select.api.md | 7 +- 82 files changed, 830 insertions(+), 317 deletions(-) create mode 100644 packages/components/icon/icon-error-state-context.ts diff --git a/AGENTS.md b/AGENTS.md index 20134bfab0..de86b8ae62 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ yarn install Setup git hooks: ```bash -yarn husky +npx husky ``` ## Package Structure @@ -152,6 +152,7 @@ You are an expert in TypeScript, Angular, and scalable web application developme - Do NOT use `ngClass`, use `class` bindings instead - Do NOT use `ngStyle`, use `style` bindings instead - When using external templates/styles, use paths relative to the component TS file. +- When a projected child depends on its host component, inject a narrow `InjectionToken` scoped to the members it uses, not the host's concrete class. ### Form Validation diff --git a/packages/components-dev/form-field/template.html b/packages/components-dev/form-field/template.html index c8603a9618..f6a26571ea 100644 --- a/packages/components-dev/form-field/template.html +++ b/packages/components-dev/form-field/template.html @@ -41,7 +41,7 @@ - + @for (option of options; track option) { @@ -55,7 +55,7 @@ - + @for (option of options; track option) { diff --git a/packages/components-dev/select/template.html b/packages/components-dev/select/template.html index a8d379d21c..1c3cba3dee 100644 --- a/packages/components-dev/select/template.html +++ b/packages/components-dev/select/template.html @@ -24,7 +24,7 @@ - + @@ -53,7 +53,7 @@ - + @@ -82,7 +82,7 @@ - + @@ -134,7 +134,7 @@ - + @@ -160,7 +160,7 @@ - + @@ -409,7 +409,7 @@ - + diff --git a/packages/components-dev/tag/template.html b/packages/components-dev/tag/template.html index 2f416d93fc..ddf3ea06b4 100644 --- a/packages/components-dev/tag/template.html +++ b/packages/components-dev/tag/template.html @@ -78,7 +78,7 @@

Tags with input

[kbqTagInputSeparatorKeyCodes]="separatorKeysCodes" (kbqTagInputTokenEnd)="inputOnCreate($event)" /> - +
@@ -105,7 +105,7 @@

Tags with autocomplete

(kbqTagInputTokenEnd)="autocompleteOnCreate($event)" /> - + @if (canCreate) { @@ -145,7 +145,7 @@

Tags with tag adding on blur disabled

(kbqTagInputTokenEnd)="enterOnCreate($event)" /> - +
diff --git a/packages/components-dev/tree-select/template.html b/packages/components-dev/tree-select/template.html index b37aa12a1e..965440c564 100644 --- a/packages/components-dev/tree-select/template.html +++ b/packages/components-dev/tree-select/template.html @@ -23,7 +23,7 @@

Custom footer

Ничего не найдено
- + diff --git a/packages/components/autocomplete/autocomplete-trigger.directive.ts b/packages/components/autocomplete/autocomplete-trigger.directive.ts index 24ef478e2d..cdd6b509b1 100644 --- a/packages/components/autocomplete/autocomplete-trigger.directive.ts +++ b/packages/components/autocomplete/autocomplete-trigger.directive.ts @@ -50,7 +50,7 @@ import { kbqResolvePanelWidth, kbqSiblingPopupProvider } from '@koobiq/components/core'; -import { KbqFormField } from '@koobiq/components/form-field'; +import { KBQ_FORM_FIELD } from '@koobiq/components/form-field'; import { Observable, Subject, Subscription, defer, fromEvent, merge, of as observableOf } from 'rxjs'; import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { KbqAutocompleteOrigin } from './autocomplete-origin.directive'; @@ -127,7 +127,7 @@ export class KbqAutocompleteTrigger private overlay = inject(Overlay); private zone = inject(NgZone); private dir = inject(Directionality, { optional: true })!; - private formField = inject(KbqFormField, { optional: true, host: true }); + private readonly formField = inject(KBQ_FORM_FIELD, { optional: true, host: true }); private viewportRuler = inject(ViewportRuler); protected readonly document = inject(DOCUMENT); diff --git a/packages/components/autocomplete/autocomplete.component.ts b/packages/components/autocomplete/autocomplete.component.ts index c0c6ec140b..0c4124d070 100644 --- a/packages/components/autocomplete/autocomplete.component.ts +++ b/packages/components/autocomplete/autocomplete.component.ts @@ -30,7 +30,7 @@ import { KbqPanelMaxWidth, KbqPanelWidth } from '@koobiq/components/core'; -import { KbqFormField } from '@koobiq/components/form-field'; +import { KBQ_FORM_FIELD } from '@koobiq/components/form-field'; import { delay, filter } from 'rxjs/operators'; /** @@ -96,7 +96,7 @@ export function KBQ_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): KbqAutocompleteDefau export class KbqAutocomplete implements AfterContentInit { private changeDetectorRef = inject(ChangeDetectorRef); private elementRef = inject>(ElementRef); - private readonly parentFormField = inject(KbqFormField, { host: true, optional: true }); + private readonly parentFormField = inject(KBQ_FORM_FIELD, { host: true, optional: true }); private readonly destroyRef = inject(DestroyRef); /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ id: string = `kbq-autocomplete-${uniqueAutocompleteIdCounter++}`; diff --git a/packages/components/core/form-field/form-field-ref.ts b/packages/components/core/form-field/form-field-ref.ts index 321e3af245..4ee4794d3d 100644 --- a/packages/components/core/form-field/form-field-ref.ts +++ b/packages/components/core/form-field/form-field-ref.ts @@ -40,6 +40,8 @@ export interface KbqFormFieldControlRef { * Internal contract for `KbqFormField` used by code that cannot import the form-field * class directly because of a circular dependency (e.g. `core/select/common.ts`). * + * @deprecated Use KbqFormField from `@koobiq/components/form-field`. + * * @docs-private */ export interface KbqFormFieldRef { @@ -56,7 +58,9 @@ export interface KbqFormFieldRef { * as alternative token to the actual `KbqFormField` class which would cause unnecessary * retention of the `KbqFormField` class and its component metadata. * - * @TODO move into form-field.ts, add correct type for `InjectionToken` (#DS-2915) + * @deprecated Use `KBQ_FORM_FIELD` from `@koobiq/components/form-field`. + * + * @docs-private */ export const KBQ_FORM_FIELD_REF = new InjectionToken('KbqFormFieldRef'); diff --git a/packages/components/datepicker/datepicker-input.directive.ts b/packages/components/datepicker/datepicker-input.directive.ts index 53bf77e387..4c30c76925 100644 --- a/packages/components/datepicker/datepicker-input.directive.ts +++ b/packages/components/datepicker/datepicker-input.directive.ts @@ -55,7 +55,7 @@ import { validationTooltipHideDelay, validationTooltipShowDelay } from '@koobiq/components/core'; -import { KbqFormField, KbqFormFieldControl } from '@koobiq/components/form-field'; +import { KBQ_FORM_FIELD, KbqFormFieldControl } from '@koobiq/components/form-field'; import type { KbqTooltipTrigger } from '@koobiq/components/tooltip'; import { Subject, Subscription } from 'rxjs'; import { KbqCalendar } from './calendar.component'; @@ -251,7 +251,7 @@ export class KbqDatepickerInput readonly adapter = inject>(DateAdapter, { optional: true })!; private readonly dateFormats = inject(KBQ_DATE_FORMATS, { optional: true }); /** @docs-private */ - protected readonly formField = inject(KbqFormField, { optional: true, host: true }); + protected readonly formField = inject(KBQ_FORM_FIELD, { optional: true, host: true }); /** @docs-private */ protected readonly localeService = inject(KBQ_LOCALE_SERVICE, { optional: true }); /** @docs-private */ diff --git a/packages/components/filter-bar/pipes/pipe-input.spec.ts b/packages/components/filter-bar/pipes/pipe-input.spec.ts index 8884518ae7..3e569cb441 100644 --- a/packages/components/filter-bar/pipes/pipe-input.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-input.spec.ts @@ -398,8 +398,8 @@ describe('KbqPipeInputComponent', () => { describe('clearing', () => { beforeEach(createFixture); - // `KbqFormField.clearValue()` and the Escape handler only reset the control; the pipe bridges that - // back into `data`. Driving `reset()` directly covers both entry points at the seam the pipe owns. + // `KbqCleaner` resets the control for pointer and Escape activation; the pipe bridges that back into + // `data`. Driving `reset()` directly covers both entry points at the seam the pipe owns. it('should propagate a control reset to data and emit onClearPipe', () => { fixture.componentInstance.activeFilter = createFilter([createPipe({ value: 'some text' })]); fixture.detectChanges(); diff --git a/packages/components/filter-bar/pipes/pipe-input.ts b/packages/components/filter-bar/pipes/pipe-input.ts index cb8d68132d..2470712396 100644 --- a/packages/components/filter-bar/pipes/pipe-input.ts +++ b/packages/components/filter-bar/pipes/pipe-input.ts @@ -45,9 +45,9 @@ export class KbqPipeInputComponent extends KbqBasePipe implements /** * Input control. * - * Must stay nullable: `reset()` — all `KbqFormField.clearValue()` and its Escape handler ever call — - * resolves to `defaultValue`, which is `null` only while `nonNullable` is unset. `{ nonNullable: true }` - * would make `reset()` yield `''` and silently break the cleaner bridge below. + * Must stay nullable: `KbqCleaner` resets the control, and `reset()` resolves to `defaultValue`, which is + * `null` only while `nonNullable` is unset. `{ nonNullable: true }` would make `reset()` yield `''` and silently + * break the cleaner bridge below. */ readonly control = new FormControl(''); @@ -83,8 +83,8 @@ export class KbqPipeInputComponent extends KbqBasePipe implements this.control.disable({ emitEvent: false }); } - // Cleaner and Escape bridge. Both `KbqFormField.clearValue()` and its Escape handler only call - // `ngControl.reset()`, which nulls the control but never touches `data.value`. `reset()` yields null + // Cleaner and Escape bridge. `KbqCleaner` calls `ngControl.reset()`, which nulls the control but never + // touches `data.value`. `reset()` yields null // while the view can only ever push a string, so `=== null` isolates those two paths from typing. this.control.valueChanges .pipe( diff --git a/packages/components/filter-bar/pipes/pipe-multi-tree-select.spec.ts b/packages/components/filter-bar/pipes/pipe-multi-tree-select.spec.ts index f5c5a1db88..b02b574266 100644 --- a/packages/components/filter-bar/pipes/pipe-multi-tree-select.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-multi-tree-select.spec.ts @@ -950,7 +950,7 @@ describe('KbqPipeMultiTreeSelectComponent', () => { const select = getPipeComponent().select(); // `KbqTreeSelect.clearValue()` empties the selection model directly, bypassing the pipe's - // `onClear()` and taking the locked values with it — hence no `kbqSelectCleaner` in the + // `onClear()` and taking the locked values with it — hence no `KbqCleaner` in the // template, which is what leaves `canShowCleaner` permanently false. expect(select.cleaner()).toBeUndefined(); expect(select.canShowCleaner).toBe(false); diff --git a/packages/components/filter-bar/pipes/pipe-tree-select.html b/packages/components/filter-bar/pipes/pipe-tree-select.html index 576ece8030..b8a8949444 100644 --- a/packages/components/filter-bar/pipes/pipe-tree-select.html +++ b/packages/components/filter-bar/pipes/pipe-tree-select.html @@ -28,7 +28,7 @@
{{ localeData.pipe.emptySearchResult }}
- + + `, + styles: ` + :host { + display: flex; + flex-direction: column; + gap: var(--kbq-size-m); + width: 320px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'data-testid': 'e2eFormFieldAddons' + } +}) +export class E2eFormFieldAddons { + protected textValue = 'Koobiq'; + protected passwordValue = 'password'; + protected numberValue = 10; + protected readonly showStepper = signal(false); +} diff --git a/packages/components/form-field/form-field.html b/packages/components/form-field/form-field.html index 30ff4c2351..15b8182725 100644 --- a/packages/components/form-field/form-field.html +++ b/packages/components/form-field/form-field.html @@ -19,12 +19,7 @@ @if (hasSuffix() || hasCleaner() || hasPasswordToggle() || hasStepper()) {
@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/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; From a32740677de153a23f13e7a5df5c4d6a4932d1db Mon Sep 17 00:00:00 2001 From: artembelik <24925224+artembelik@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:59:03 +0000 Subject: [PATCH 2/2] test: updated e2e snapshots --- .../input/__screenshots__/04-light.png | Bin 13701 -> 13749 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/components/input/__screenshots__/04-light.png b/packages/components/input/__screenshots__/04-light.png index 1db569eb022b881913e959e2a0176f3b05df1dc9..8346ece5e1a67b79c08d79c2b2c6f917f7529935 100644 GIT binary patch literal 13749 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 literal 13701 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