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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/docs/src/app/services/i18n.characterization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { KbqToastService } from '@koobiq/components/toast';
import { KBQ_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER } from '@koobiq/components/tooltip';
import { BehaviorSubject, map } from 'rxjs';
import { DocsCopyButtonComponent } from '../components/copy-button/copy-button';
import { DocsTokensTable } from '../components/design-tokens-viewers/tokens-overview';
Expand Down Expand Up @@ -86,7 +85,7 @@ describe('docs i18n strings (characterization)', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [DocsTokensTable],
providers: [provideDocsLocale(locale), provideRouter([]), KBQ_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER]
providers: [provideDocsLocale(locale), provideRouter([])]
});

const fixture = TestBed.createComponent(DocsTokensTable);
Expand Down
4 changes: 1 addition & 3 deletions apps/docs/src/assets/stackblitz/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { KBQ_LOCALE_SERVICE, KbqLocaleService } from '@koobiq/components/core';
import { KBQ_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER } from '@koobiq/components/tooltip';
import { KoobiqDocsExample } from './example/koobiq-docs-example';

bootstrapApplication(KoobiqDocsExample, {
providers: [
provideAnimations(),
provideHttpClient(),
provideRouter([]),
{ provide: KBQ_LOCALE_SERVICE, useClass: KbqLocaleService },
KBQ_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER

@artembelik artembelik Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в прошлый раз мы это добавили, тк тултипы на стакблитз не отображались DS-5007

UPD. стакблитз запускается, ошибок нет

{ provide: KBQ_LOCALE_SERVICE, useClass: KbqLocaleService }
]
}).catch((err) => console.error(err));
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,17 @@ import { KbqAutocomplete } from './autocomplete.component';
/** The total height of the autocomplete panel. */
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;

/** Injection token that determines the scroll handling while the autocomplete panel is open. */
/**
* Injection token that determines the scroll handling while the autocomplete panel is open. The root default
* keeps the trigger usable outside `KbqAutocompleteModule`'s injector; providing the token anywhere still wins
* over it.
*/
export const KBQ_AUTOCOMPLETE_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
'kbq-autocomplete-scroll-strategy'
'kbq-autocomplete-scroll-strategy',
{
providedIn: 'root',
factory: () => KBQ_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY(inject(Overlay))
}
);

export function KBQ_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy {
Expand Down
6 changes: 1 addition & 5 deletions packages/components/autocomplete/autocomplete.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { OverlayModule } from '@angular/cdk/overlay';
import { NgModule } from '@angular/core';
import { KbqOptionModule } from '@koobiq/components/core';
import { KbqAutocompleteOrigin } from './autocomplete-origin.directive';
import {
KBQ_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER,
KbqAutocompleteTrigger
} from './autocomplete-trigger.directive';
import { KbqAutocompleteTrigger } from './autocomplete-trigger.directive';
import { KbqAutocomplete, KbqAutocompleteFooter } from './autocomplete.component';

@NgModule({
Expand All @@ -17,7 +14,6 @@ import { KbqAutocomplete, KbqAutocompleteFooter } from './autocomplete.component
KbqAutocompleteOrigin,
KbqAutocompleteFooter
],
providers: [KBQ_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER],
exports: [
KbqAutocomplete,
KbqOptionModule,
Expand Down
9 changes: 3 additions & 6 deletions packages/components/button-toggle/button-toggle.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import { KbqTitleModule } from '@koobiq/components/title';
import { KbqButtonToggle, KbqButtonToggleGroup } from './button-toggle.component';

/**
* Entry point for `KbqButtonToggleGroup`/`KbqButtonToggle`.
*
* Both are standalone, but importing them directly is not enough: a toggle renders `kbq-title` in its
* own template, and the tooltip behind it resolves `KBQ_TOOLTIP_SCROLL_STRATEGY` and
* `KBQ_TOOLTIP_OPEN_TIME` from the module injector, where only `KbqTitleModule` puts them. Import
* this module — a bare `imports: [KbqButtonToggle]` throws NG0201 the moment a label is truncated.
* Entry point for `KbqButtonToggleGroup`/`KbqButtonToggle`. Both are standalone, so a bare
* `imports: [KbqButtonToggle]` works; this module is the convenience bundle that also pulls in
* `KbqButtonModule`/`KbqTitleModule` and re-exports the slot markers.
*/
@NgModule({
imports: [KbqButtonModule, KbqTitleModule, KbqButtonToggleGroup, KbqButtonToggle],
Expand Down
16 changes: 13 additions & 3 deletions packages/components/core/select/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Overlay, RepositionScrollStrategy, ScrollStrategy } from '@angular/cdk/overlay';
import { InjectionToken } from '@angular/core';
import { inject, InjectionToken } from '@angular/core';

/**
* Minimum option count threshold for displaying select search.
Expand All @@ -13,8 +13,18 @@ export const KBQ_SELECT_SEARCH_MIN_OPTIONS_THRESHOLD = 10;
*/
export const SELECT_PANEL_VIEWPORT_PADDING = 8;

/** Injection token that determines the scroll handling while a select is open. */
export const KBQ_SELECT_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>('kbq-select-scroll-strategy');
/**
* Injection token that determines the scroll handling while a select is open.
*
* The root default keeps the select usable when it is reached outside `KbqSelectModule`'s injector - imported
* as a bare standalone component, pulled in through another standalone component (`KbqTimezoneSelect`,
* `KbqCalendarHeader`), or rendered in a component built from the root injector, as `KbqModalService` does.
* Providing the token anywhere still wins over this default.
*/
export const KBQ_SELECT_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>('kbq-select-scroll-strategy', {
providedIn: 'root',
factory: () => kbqSelectScrollStrategyProviderFactory(inject(Overlay))
});

/** @docs-private */
export function kbqSelectScrollStrategyProviderFactory(overlay: Overlay): () => RepositionScrollStrategy {
Expand Down
15 changes: 4 additions & 11 deletions packages/components/datepicker/calendar-header.component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { TitleCasePipe } from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
inject,
Input,
output,
ViewEncapsulation
} from '@angular/core';
import { AfterContentInit, ChangeDetectionStrategy, Component, Input, output, ViewEncapsulation } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { DateAdapter, kbqInjectA11yLocaleConfiguration, KbqOptionModule } from '@koobiq/components/core';
import { kbqInjectA11yLocaleConfiguration, KbqOptionModule } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqSelectModule } from '@koobiq/components/select';
import { injectRequiredDateAdapter } from './datepicker-errors';

const defaultMinYear = 1900;
const defaultMaxYear = 2099;
Expand Down Expand Up @@ -43,7 +36,7 @@ export type MonthName = {
exportAs: 'kbqCalendarHeader'
})
export class KbqCalendarHeader<D> implements AfterContentInit {
private readonly adapter = inject<DateAdapter<D>>(DateAdapter);
private readonly adapter = injectRequiredDateAdapter<D>();

/** Accessible names for the icon-only month navigation buttons. */
protected readonly a11yLocaleConfiguration = kbqInjectA11yLocaleConfiguration();
Expand Down
12 changes: 12 additions & 0 deletions packages/components/datepicker/calendar-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { By } from '@angular/platform-browser';
import { KbqLuxonDateModule, LuxonDateAdapter } from '@koobiq/angular-luxon-adapter/adapter';
import { DateAdapter } from '@koobiq/components/core';
import { DateTime } from 'luxon';
import { KbqCalendarHeader } from './calendar-header.component';
import { KbqCalendar } from './calendar.component';
import { KbqDatepickerIntl } from './datepicker-intl';
import { KbqDatepickerModule } from './datepicker-module';
Expand Down Expand Up @@ -67,6 +68,17 @@ describe('KbqCalendarHeader', () => {
expect(adapter.format(calendarInstance.activeDate, 'yyyyMMdd')).toEqual('20170128');
});
});

describe('without a DateAdapter', () => {
it('should say which provider is missing', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({ imports: [KbqCalendarHeader] });

expect(() => TestBed.createComponent(KbqCalendarHeader)).toThrow(
/KbqDatepicker: No provider found for DateAdapter/
);
});
});
});

@Component({
Expand Down
9 changes: 2 additions & 7 deletions packages/components/datepicker/calendar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import {
output,
viewChild
} from '@angular/core';
import { DateAdapter } from '@koobiq/components/core';
import { Subject, Subscription } from 'rxjs';
import { KbqCalendarCellCssClasses } from './calendar-body.component';
import { KbqCalendarHeader } from './calendar-header.component';
import { createMissingDateImplError } from './datepicker-errors';
import { injectRequiredDateAdapter } from './datepicker-errors';
import { KbqDatepickerIntl } from './datepicker-intl';
import { KbqMonthView } from './month-view.component';

Expand All @@ -41,7 +40,7 @@ import { KbqMonthView } from './month-view.component';
exportAs: 'kbqCalendar'
})
export class KbqCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
private readonly adapter = inject<DateAdapter<D>>(DateAdapter, { optional: true })!;
private readonly adapter = injectRequiredDateAdapter<D>();
private changeDetectorRef = inject(ChangeDetectorRef);

/** A date representing the period (month or year) to start the calendar in. */
Expand Down Expand Up @@ -167,10 +166,6 @@ export class KbqCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
const intl = inject(KbqDatepickerIntl);
const changeDetectorRef = this.changeDetectorRef;

if (!this.adapter) {
throw createMissingDateImplError('DateAdapter');
}

this.intlChanges = intl.changes.subscribe(() => {
changeDetectorRef.markForCheck();
this.stateChanges.next();
Expand Down
14 changes: 14 additions & 0 deletions packages/components/datepicker/datepicker-errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { inject } from '@angular/core';
import { DateAdapter } from '@koobiq/components/core';

/** @docs-private */
export function createMissingDateImplError(provider: string) {
return Error(
`KbqDatepicker: No provider found for ${provider}. You must import one of the existing ` +
`modules at your application root or provide a custom implementation or use exists ones.`
);
}

/** Injects `DateAdapter`, naming the missing provider instead of letting DI throw a bare `NullInjectorError`. */
export function injectRequiredDateAdapter<D>(): DateAdapter<D> {
const adapter = inject<DateAdapter<D>>(DateAdapter, { optional: true });

if (!adapter) {
throw createMissingDateImplError('DateAdapter');
}

return adapter;
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { KBQ_FORM_FIELD, KbqFormFieldControl } from '@koobiq/components/form-fie
import type { KbqTooltipTrigger } from '@koobiq/components/tooltip';
import { Subject, Subscription } from 'rxjs';
import { KbqCalendar } from './calendar.component';
import { createMissingDateImplError } from './datepicker-errors';
import { injectRequiredDateAdapter } from './datepicker-errors';
import { KbqDatepicker } from './datepicker.component';

enum DateParts {
Expand Down Expand Up @@ -248,7 +248,7 @@ export class KbqDatepickerInput<D>
{
elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
private readonly renderer = inject(Renderer2);
readonly adapter = inject<DateAdapter<D>>(DateAdapter, { optional: true })!;
readonly adapter: DateAdapter<D> = injectRequiredDateAdapter<D>();
private readonly dateFormats = inject<KbqDateFormats>(KBQ_DATE_FORMATS, { optional: true });
/** @docs-private */
protected readonly formField = inject(KBQ_FORM_FIELD, { optional: true, host: true });
Expand Down Expand Up @@ -565,10 +565,6 @@ export class KbqDatepickerInput<D>
this.filterValidator
]);

if (!this.adapter) {
throw createMissingDateImplError('DateAdapter');
}

this.errorStateTracker = new KbqErrorStateTracker(
inject(ErrorStateMatcher),
// update ngControl later, so it will be initialized
Expand Down
11 changes: 2 additions & 9 deletions packages/components/datepicker/datepicker-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import { KbqCalendar } from './calendar.component';
import { KbqDatepickerInput } from './datepicker-input.directive';
import { KbqDatepickerIntl } from './datepicker-intl';
import { KbqDatepickerToggleIcon, KbqDatepickerToggleIconComponent } from './datepicker-toggle.component';
import {
KBQ_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER,
KbqDatepicker,
KbqDatepickerContent
} from './datepicker.component';
import { KbqDatepicker, KbqDatepickerContent } from './datepicker.component';
import { KbqMonthView } from './month-view.component';

@NgModule({
Expand All @@ -39,10 +35,7 @@ import { KbqMonthView } from './month-view.component';
KbqMonthView,
KbqCalendarHeader
],
providers: [
KbqDatepickerIntl,
KBQ_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER
],
providers: [KbqDatepickerIntl],
exports: [
KbqCalendar,
KbqCalendarBody,
Expand Down
29 changes: 13 additions & 16 deletions packages/components/datepicker/datepicker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,29 @@ import {
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
DateAdapter,
KBQ_CONNECTED_OVERLAY_ABOVE_CLASS,
KBQ_CONNECTED_OVERLAY_BELOW_CLASS
} from '@koobiq/components/core';
import { KBQ_CONNECTED_OVERLAY_ABOVE_CLASS, KBQ_CONNECTED_OVERLAY_BELOW_CLASS } from '@koobiq/components/core';
import { KbqFormFieldControl } from '@koobiq/components/form-field';
import { merge, Subject, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { KbqCalendarCellCssClasses } from './calendar-body.component';
import { KbqCalendar } from './calendar.component';
import { kbqDatepickerAnimations } from './datepicker-animations';
import { createMissingDateImplError } from './datepicker-errors';
import { injectRequiredDateAdapter } from './datepicker-errors';
import { KbqDatepickerInput } from './datepicker-input.directive';

/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;

/** Injection token that determines the scroll handling while the calendar is open. */
/**
* Injection token that determines the scroll handling while the calendar is open. The root default keeps the
* datepicker usable outside `KbqDatepickerModule`'s injector; providing the token anywhere still wins over it.
*/
export const KBQ_DATEPICKER_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
'kbq-datepicker-scroll-strategy'
'kbq-datepicker-scroll-strategy',
{
providedIn: 'root',
factory: () => KBQ_DATEPICKER_SCROLL_STRATEGY_FACTORY(inject(Overlay))
}
);

/** @docs-private */
Expand Down Expand Up @@ -134,7 +137,7 @@ export class KbqDatepicker<D> implements OnDestroy {
private overlay = inject(Overlay);
private ngZone = inject(NgZone);
private viewContainerRef = inject(ViewContainerRef);
private readonly dateAdapter = inject<DateAdapter<D>>(DateAdapter, { optional: true })!;
private readonly dateAdapter = injectRequiredDateAdapter<D>();
private dir = inject(Directionality, { optional: true })!;

protected readonly document = inject<Document>(DOCUMENT);
Expand Down Expand Up @@ -295,13 +298,7 @@ export class KbqDatepicker<D> implements OnDestroy {
private closeSubscription = Subscription.EMPTY;

constructor() {
const scrollStrategy = inject(KBQ_DATEPICKER_SCROLL_STRATEGY);

if (!this.dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}

this.scrollStrategy = scrollStrategy;
this.scrollStrategy = inject(KBQ_DATEPICKER_SCROLL_STRATEGY);
}

ngOnDestroy() {
Expand Down
Loading