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
22 changes: 21 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.

Every schematic named below runs automatically:

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

#### Link

The three inputs the automated signal migration skipped were all accessors, and each did something beyond storing a value: `disabled` wrote a separate signal, `tabIndex` folded in the disabled state, and `print` was a setter with no getter that also computed the printed URL.

`disabledSignal` stays a public `WritableSignal<boolean>` — `kbqTooltip` accepts a link through `forDisabledComponent` and reads it. It is a `linkedSignal` over the `disabled` input now, so binding still drives it and a direct write still wins.

| Pattern | Manual migration |
| ------------------------------- | -------------------------------------------------------------------------- |
| `.disabled` | Read as `disabled()` — rewritten for you |
| `.tabIndex` | `tabIndex()`, and expect what was bound — not `-1` for a disabled link |
| `.print = …` | Bind `[print]`; it was a setter with no getter, so there is no read to fix |
| `.icons` / `.icon` / `.hasIcon` | Now `protected`/`private`; the icon spacing classes are the contract |
| `.printMode` / `.printUrl` | Now `protected`; the `kbq-link_print` class and `print` attribute are |
Comment on lines +1058 to +1064

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.

Truncated cell — the sentence ends on a dangling verb: "the kbq-link_print class and print attribute are".

The RU counterpart at migration.ru.md:1068 is complete ("контракт — класс kbq-link_print и атрибут print"), so the predicate was lost only on the EN side. packages/schematics/src/migrations/link-signals/README.md:40 carries the identical truncated cell.

The suggestion spans the whole table because prettier realigns every row once the last cell grows — this is prettier --parser markdown --print-width 120 output, so it is check-clean:

Suggested change
| Pattern | Manual migration |
| ------------------------------- | -------------------------------------------------------------------------- |
| `.disabled` | Read as `disabled()` — rewritten for you |
| `.tabIndex` | `tabIndex()`, and expect what was bound — not `-1` for a disabled link |
| `.print = …` | Bind `[print]`; it was a setter with no getter, so there is no read to fix |
| `.icons` / `.icon` / `.hasIcon` | Now `protected`/`private`; the icon spacing classes are the contract |
| `.printMode` / `.printUrl` | Now `protected`; the `kbq-link_print` class and `print` attribute are |
| Pattern | Manual migration |
| ------------------------------- | ---------------------------------------------------------------------------------- |
| `.disabled` | Read as `disabled()` — rewritten for you |
| `.tabIndex` | `tabIndex()`, and expect what was bound — not `-1` for a disabled link |
| `.print = …` | Bind `[print]`; it was a setter with no getter, so there is no read to fix |
| `.icons` / `.icon` / `.hasIcon` | Now `protected`/`private`; the icon spacing classes are the contract |
| `.printMode` / `.printUrl` | Now `protected`; the `kbq-link_print` class and `print` attribute are the contract |

Separately, one line up in the same section — migration.en.md:1038 contradicts its own subsection list. It now reads:

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.

But the subsections under ### 18 are Link (1052), Search expandable (1072), Split button (1094) and Title (1116) — three of the four are named in the wave-1 list in the same sentence. Only Link belongs to the second wave. migration.ru.md:1042 carries the identical error, so both files are in sync and both wrong; the sentence this replaced was consistent.


The host attribute still goes to `-1` while the link is disabled, so nothing about focus behavior changed — only a programmatic read of `tabIndex` sees the difference.

**`[print]="undefined"` no longer marks the link as printable.** The old setter tested `value !== null`, so an explicit `undefined` passed it: the link got `kbq-link_print` and printed its `href`. The input tests `!= null`, which covers both. An unbound link behaves exactly as before — no class, and the href still lands in the `print` attribute. `print` accepts `string | null` instead of `any`.

Handled by `link-signals`: the `disabled` 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
22 changes: 21 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,26 @@ ng update @koobiq/components@20
ng g @koobiq/components:<schematic-name> --project <your project>
```

#### Link

Три входа, которые автоматическая миграция на сигналы пропустила, были геттерами и сеттерами, и каждый делал больше, чем просто хранил значение: `disabled` писал в отдельный сигнал, `tabIndex` подмешивал состояние блокировки, а `print` был сеттером без геттера, который ещё и вычислял печатаемый URL.

`disabledSignal` остаётся публичным `WritableSignal<boolean>` — `kbqTooltip` принимает ссылку через `forDisabledComponent` и читает его. Теперь это `linkedSignal` поверх входа `disabled`, поэтому привязка по-прежнему им управляет, а прямая запись по-прежнему побеждает.

| Что было | Как мигрировать вручную |
| ------------------------------- | ---------------------------------------------------------------------------------- |
| `.disabled` | Читать как `disabled()` — переписывается за вас |
| `.tabIndex` | `tabIndex()`, и ожидать привязанное значение, а не `-1` для заблокированной ссылки |
| `.print = …` | Привязать `[print]`; это был сеттер без геттера, поэтому чтений нет |
| `.icons` / `.icon` / `.hasIcon` | Стали `protected`/`private`; контракт — классы отступов у иконок |
| `.printMode` / `.printUrl` | Стали `protected`; контракт — класс `kbq-link_print` и атрибут `print` |

Атрибут на хосте по-прежнему становится `-1`, пока ссылка заблокирована, так что поведение фокуса не изменилось — разницу видит только программное чтение `tabIndex`.

**`[print]="undefined"` больше не помечает ссылку как печатаемую.** Старый сеттер проверял `value !== null`, поэтому явный `undefined` эту проверку проходил: ссылка получала класс `kbq-link_print` и печатала свой `href`. Вход проверяет `!= null`, что покрывает оба случая. Ссылка без привязки ведёт себя ровно как раньше: класса нет, а `href` по-прежнему попадает в атрибут `print`. `print` принимает `string | null` вместо `any`.

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

#### Search expandable

Шаг 4 уже переименовывает элемент `kbq-filter-search` в `kbq-search-expandable`. Но та замена трогала только тег, поэтому входы удалённого `KbqFilterBarSearch` оставались в разметке атрибутами, которых у нового компонента нет, — и молча, потому что неизвестный атрибут на компоненте не является ошибкой. Теперь `v20-upgrade` переименовывает и их:
Expand Down
98 changes: 91 additions & 7 deletions packages/components/link/link.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLinkModule } from './index';
import { KbqLink, KbqLinkModule } from './index';

describe('KbqLink', () => {
beforeEach(() => {
Expand All @@ -13,7 +14,8 @@ describe('KbqLink', () => {
KbqLinkIconTestApp,
KbqLinkPrintTestApp,
KbqLinkPseudoTestApp,
KbqLinkNoUnderlineTestApp
KbqLinkNoUnderlineTestApp,
KbqLinkDisabledTestApp
]
}).compileComponents();
});
Expand Down Expand Up @@ -44,6 +46,9 @@ describe('KbqLink', () => {
it('should has .kbq-link_print', fakeAsync(() => {
const fixture = TestBed.createComponent(KbqLinkPrintTestApp);

// `print` falls back to the host `href`, which is DOM state: it is read in a microtask after the
// first render, so the attribute lands on the change detection pass that follows.
fixture.detectChanges();
tick();
fixture.detectChanges();

Expand All @@ -52,13 +57,37 @@ describe('KbqLink', () => {
expect(link.classList).toContain('kbq-link_print');
expect(link.attributes.print.nodeValue).toContain('localhost:3003/');

fixture.componentInstance.print = 'newUrl';
fixture.componentInstance.print.set('newUrl');
fixture.detectChanges();
tick();
fixture.detectChanges();

expect(link.attributes.print.nodeValue).toBe('newUrl');
}));

it('should drop .kbq-link_print when print is unbound', fakeAsync(() => {
const fixture = TestBed.createComponent(KbqLinkBaseTestApp);

fixture.detectChanges();
tick();
fixture.detectChanges();

expect(link.attributes.print.nodeValue).toContain(fixture.componentInstance.print);
const link = fixture.nativeElement.querySelector('[kbq-link]');

expect(link.classList).not.toContain('kbq-link_print');
// Unchanged from before the review: the href still lands in the attribute, only the class is absent.
expect(link.attributes.print.nodeValue).toContain('localhost:3003/');
}));

it('should treat an explicit undefined print as not printable', fakeAsync(() => {
const fixture = TestBed.createComponent(KbqLinkPrintTestApp);

fixture.componentInstance.print.set(undefined);
fixture.detectChanges();
tick();
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('[kbq-link]').classList).not.toContain('kbq-link_print');
}));

it('should has .kbq-link_pseudo', () => {
Expand All @@ -80,6 +109,50 @@ describe('KbqLink', () => {

expect(link.classList).toContain('kbq-link_no-underline');
});
it('should take a disabled link out of the tab order', () => {
const fixture = TestBed.createComponent(KbqLinkDisabledTestApp);

fixture.detectChanges();

const link = fixture.nativeElement.querySelector('[kbq-link]');

expect(link.attributes.tabIndex.nodeValue).toBe('5');
expect(link.classList).not.toContain('kbq-disabled');

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

expect(link.attributes.tabIndex.nodeValue).toBe('-1');
expect(link.classList).toContain('kbq-disabled');
expect(link.getAttribute('disabled')).toBe('true');
});

it('should keep reporting the bound tabIndex while disabled', () => {
const fixture = TestBed.createComponent(KbqLinkDisabledTestApp);

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

const link = fixture.debugElement.query(By.directive(KbqLink)).injector.get(KbqLink);

expect(link.tabIndex()).toBe(5);
expect(link.disabled()).toBe(true);
});

it('should let a forDisabledComponent consumer drive disabledSignal', () => {
const fixture = TestBed.createComponent(KbqLinkDisabledTestApp);

fixture.detectChanges();

const linkElement = fixture.nativeElement.querySelector('[kbq-link]');
const link = fixture.debugElement.query(By.directive(KbqLink)).injector.get(KbqLink);

link.disabledSignal.set(true);
fixture.detectChanges();

expect(linkElement.classList).toContain('kbq-disabled');
});
});

@Component({
Expand All @@ -95,11 +168,11 @@ class KbqLinkBaseTestApp {}
selector: 'kbq-link-print-test-app',
imports: [KbqLinkModule, KbqIconModule],
template: `
<a href="http://localhost:3003/" kbq-link [print]="print">Отчет сканирования</a>
<a href="http://localhost:3003/" kbq-link [print]="print()">Отчет сканирования</a>
`
})
class KbqLinkPrintTestApp {
print: string = '';
readonly print = signal<string | null | undefined>('');
}

@Component({
Expand Down Expand Up @@ -131,3 +204,14 @@ class KbqLinkPseudoTestApp {}
`
})
class KbqLinkNoUnderlineTestApp {}

@Component({
selector: 'kbq-link-disabled-test-app',
imports: [KbqLinkModule],
template: `
<a href="http://localhost:3003/" kbq-link [disabled]="disabled()" [tabIndex]="5">Отчет сканирования</a>
`
})
class KbqLinkDisabledTestApp {
readonly disabled = signal(false);
}
Loading