Skip to content
Draft
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
19 changes: 18 additions & 1 deletion docs/guides/migration.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ for each option it deselected and reporting the shortened value to the form cont

### 18. Component review (20.3.0)

Ten components went through a full review in 20.3.0: notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.
Components went through a full review in 20.3.0, in two waves. The first covered notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select; the second is the one each subsection below belongs to. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.

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.

This sentence contradicts three of the four subsections it introduces.

Section 18's subsections are #### Loader overlay (1052), #### Search expandable (1069), #### Split button (1091) and #### Title (1113). Search-expandable, split-button and title are all named in the first-wave list in this same sentence, yet "the second is the one each subsection below belongs to" assigns them to wave two. Loader overlay is named in neither list.

A reader migrating KbqSplitButton is told split-button was reviewed in wave one and, one clause later, that the #### Split button subsection belongs to wave two. And section 18 keeps growing — it went from 3 to 4 subsections in this PR — so every future addition either re-breaks the sentence or forces another rewrite of it. Wave membership is per-subsection data being encoded in a shared intro.

The original one-liner with loader-overlay added is self-consistent and has nothing to go stale:

Suggested change
Components went through a full review in 20.3.0, in two waves. The first covered notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select; the second is the one each subsection below belongs to. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.
Eleven components went through a full review in 20.3.0: loader-overlay, notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.

docs/guides/migration.ru.md:1042 carries the identical defect ("ко второй относится каждый из подразделов ниже") and needs the mirrored change.


Every schematic named below runs automatically:

Expand All @@ -1049,6 +1049,23 @@ Most of them report rather than rewrite: what replaces a removed member or a sig
ng g @koobiq/components:<schematic-name> --project <your project>
```

#### Loader overlay

`text` and `caption` were the two inputs the automated signal migration skipped — it saw them read inside `@if` blocks and would not risk the narrowing. They are `input()` now, and honest about being optional: both were declared `string` over a field with no initializer, so an overlay that bound neither reported `undefined` from a non-nullable type.

| Pattern | Manual migration |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `.text` / `.caption` | Read as `text()` / `caption()` — rewritten for you |
| `.text = …` | Bind `[text]` in the template — the input is read-only |
| `.isEmpty` / `.isExternalIndicator` / `.isExternalText` / `.isExternalCaption` / `.spinnerSize` | Now `protected`; what the overlay renders is the contract, not how it decides |
| `.externalIndicator` / `.externalText` / `.externalCaption` | Now `private` signal queries |

**`transparent` is a `booleanAttribute` input now.** `<kbq-loader-overlay transparent>` used to pass the empty string, which is falsy, so the valueless attribute rendered the _filled_ background — the opposite of how it reads. It means `true` now, and `[transparent]="'false'"` means `false`.

`size` and `card` were already signals in 20.2.0 and did not change.

Handled by `loader-overlay-signals`: the `text` and `caption` reads are rewritten, the rest is reported.

#### Search expandable

Step 4 already renames the `kbq-filter-search` element to `kbq-search-expandable`. That rewrite only ever touched the tag, so the inputs of the removed `KbqFilterBarSearch` survived as attributes the new component does not have — silently, because an unknown attribute on a component is not an error. `v20-upgrade` renames them too now:
Expand Down
19 changes: 18 additions & 1 deletion docs/guides/migration.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ ng g @koobiq/components:list-tree-multiple-input --project <your project>

### 18. Ревью компонентов (20.3.0)

В 20.3.0 полное ревью прошли десять компонентов: notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree и tree-select. Каждое ревью закрывало члены, которые никогда не были частью контракта компонента, переводило входы на сигналы там, где в этом и был его смысл, и попутно исправляло найденные ошибки поведения. Ниже перечислено только то, что доходит до потребителя.
В 20.3.0 полное ревью прошло в две волны. В первую вошли notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree и tree-select; ко второй относится каждый из подразделов ниже. Каждое ревью закрывало члены, которые никогда не были частью контракта компонента, переводило входы на сигналы там, где в этом и был его смысл, и попутно исправляло найденные ошибки поведения. Ниже перечислено только то, что доходит до потребителя.

Все схематики, названные ниже, запускаются автоматически:

Expand All @@ -1053,6 +1053,23 @@ ng update @koobiq/components@20
ng g @koobiq/components:<schematic-name> --project <your project>
```

#### Loader overlay

`text` и `caption` — те два входа, которые автоматическая миграция на сигналы пропустила: они читаются внутри блоков `@if`, и миграция не стала рисковать сужением типов. Теперь это `input()`, и они честно необязательные: оба были объявлены как `string` поверх поля без инициализатора, поэтому оверлей без обеих привязок возвращал `undefined` из ненулевого типа.

| Что было | Как мигрировать вручную |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `.text` / `.caption` | Читать как `text()` / `caption()` — переписывается за вас |
| `.text = …` | Привязать `[text]` в шаблоне — вход доступен только на чтение |
| `.isEmpty` / `.isExternalIndicator` / `.isExternalText` / `.isExternalCaption` / `.spinnerSize` | Стали `protected`; контракт — это то, что оверлей отрисовывает, а не то, как он выбирает |
| `.externalIndicator` / `.externalText` / `.externalCaption` | Стали `private` сигнальными запросами |

**`transparent` теперь `booleanAttribute`-вход.** `<kbq-loader-overlay transparent>` раньше передавал пустую строку, которая ложна, поэтому атрибут без значения давал _заливку_ — противоположность тому, как он читается. Теперь он означает `true`, а `[transparent]="'false'"` — `false`.

`size` и `card` стали сигналами ещё в 20.2.0 и не изменились.

Закрывается схематиком `loader-overlay-signals`: чтения `text` и `caption` переписываются, остальное сообщается в отчёте.

#### Search expandable

Шаг 4 уже переименовывает элемент `kbq-filter-search` в `kbq-search-expandable`. Но та замена трогала только тег, поэтому входы удалённого `KbqFilterBarSearch` оставались в разметке атрибутами, которых у нового компонента нет, — и молча, потому что неизвестный атрибут на компоненте не является ошибкой. Теперь `v20-upgrade` переименовывает и их:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<div class="kbq-loader-overlay__container">
@if (isExternalIndicator) {
@if (isExternalIndicator()) {
<ng-content select="[kbq-loader-overlay-indicator]" />
} @else {
<kbq-progress-spinner [mode]="'indeterminate'" [size]="spinnerSize" />
<kbq-progress-spinner [mode]="'indeterminate'" [size]="spinnerSize()" />
}

@if (isExternalText) {
@if (isExternalText()) {
<ng-content select="[kbq-loader-overlay-text]" />
} @else {
@if (text) {
@if (text(); as textValue) {
<div class="kbq-loader-overlay-text">
{{ text }}
{{ textValue }}
</div>
}
}

@if (isExternalCaption) {
@if (isExternalCaption()) {
<ng-content select="[kbq-loader-overlay-caption]" />
} @else {
@if (caption) {
@if (caption(); as captionValue) {
<div class="kbq-loader-overlay-caption">
{{ caption }}
{{ captionValue }}
</div>
}
}
Expand Down
119 changes: 116 additions & 3 deletions packages/components/loader-overlay/loader-overlay.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, viewChild } from '@angular/core';
import { Component, signal, viewChild } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ThemePalette } from '@koobiq/components/core';
import { KbqDefaultSizes, ThemePalette } from '@koobiq/components/core';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';
import {
KbqLoaderOverlay,
Expand All @@ -19,7 +19,10 @@ describe('KbqLoaderOverlay', () => {
KbqLoaderOverlayModule,
OverlayWithParams,
OverlayNoParams,
OverlayWithExternalParams
OverlayWithExternalParams,
OverlayWithValuelessTransparent,
OverlayWithChangingText,
OverlayWithSize
]
}).compileComponents();
});
Expand Down Expand Up @@ -83,6 +86,84 @@ describe('KbqLoaderOverlay', () => {

expect(fixture.debugElement.query(By.directive(KbqLoaderOverlay)).classes).toMatchSnapshot();
});
it('should treat a valueless transparent attribute as true', () => {
const fixture = TestBed.createComponent(OverlayWithValuelessTransparent);

fixture.detectChanges();

const host = fixture.debugElement.query(By.directive(KbqLoaderOverlay)).nativeElement as HTMLElement;

expect(host.classList).toContain('kbq-loader-overlay_transparent');
expect(host.classList).not.toContain('kbq-loader-overlay_filled');
});

it('should stop being empty once the text arrives', () => {
const fixture = TestBed.createComponent(OverlayWithChangingText);

fixture.detectChanges();

const host = fixture.debugElement.query(By.directive(KbqLoaderOverlay)).nativeElement as HTMLElement;

expect(host.classList).toContain('kbq-loader-overlay_empty');

fixture.componentInstance.text.set('Загрузка');
fixture.detectChanges();

expect(host.classList).not.toContain('kbq-loader-overlay_empty');
expect(host.querySelector('.kbq-loader-overlay-text')!.textContent!.trim()).toBe('Загрузка');
});

it('should report undefined for an unbound text and caption', () => {
const fixture = TestBed.createComponent(OverlayNoParams);

fixture.detectChanges();

const overlay = fixture.debugElement.query(By.directive(KbqLoaderOverlay))
.componentInstance as KbqLoaderOverlay;

expect(overlay.text()).toBeUndefined();
expect(overlay.caption()).toBeUndefined();
});
it('should map every size to a class and a spinner size', () => {
const fixture = TestBed.createComponent(OverlayWithSize);

fixture.detectChanges();

const host = fixture.debugElement.query(By.directive(KbqLoaderOverlay)).nativeElement as HTMLElement;
const spinner = fixture.nativeElement.querySelector('kbq-progress-spinner') as HTMLElement;

expect(host.classList).toContain('kbq-loader-overlay_big');
expect(spinner.classList).toContain('kbq-progress-spinner_big');

fixture.componentInstance.size.set('normal');
fixture.detectChanges();

expect(host.classList).toContain('kbq-loader-overlay_normal');
expect(host.classList).not.toContain('kbq-loader-overlay_big');
// There is no `normal` progress spinner, so the overlay keeps the big indicator for it.
expect(spinner.classList).toContain('kbq-progress-spinner_big');

fixture.componentInstance.size.set('compact');
fixture.detectChanges();

expect(host.classList).toContain('kbq-loader-overlay_compact');
expect(spinner.classList).not.toContain('kbq-progress-spinner_big');
});

it('should mark the overlay as a card', () => {
const fixture = TestBed.createComponent(OverlayWithSize);

fixture.detectChanges();

const host = fixture.debugElement.query(By.directive(KbqLoaderOverlay)).nativeElement as HTMLElement;

expect(host.classList).not.toContain('kbq-loader-overlay_card');

fixture.componentInstance.card.set(true);
fixture.detectChanges();

expect(host.classList).toContain('kbq-loader-overlay_card');
});
});

@Component({
Expand Down Expand Up @@ -136,3 +217,35 @@ class OverlayNoParams {}
class OverlayWithExternalParams {
themePalette = ThemePalette;
}

@Component({
selector: 'overlay-with-valueless-transparent',
imports: [KbqProgressSpinnerModule, KbqLoaderOverlayModule],
template: `
<kbq-loader-overlay transparent />
`
})
class OverlayWithValuelessTransparent {}

@Component({
selector: 'overlay-with-changing-text',
imports: [KbqProgressSpinnerModule, KbqLoaderOverlayModule],
template: `
<kbq-loader-overlay [text]="text()" />
`
})
class OverlayWithChangingText {
readonly text = signal<string | undefined>(undefined);
}

@Component({
selector: 'overlay-with-size',
imports: [KbqProgressSpinnerModule, KbqLoaderOverlayModule],
template: `
<kbq-loader-overlay [size]="size()" [card]="card()" />
`
})
class OverlayWithSize {
readonly size = signal<KbqDefaultSizes>('big');
readonly card = signal(false);
}
Loading