diff --git a/packages/components-dev/dl/module.ts b/packages/components-dev/dl/module.ts index 1cf276b09a..5f7ed689ff 100644 --- a/packages/components-dev/dl/module.ts +++ b/packages/components-dev/dl/module.ts @@ -1,15 +1,27 @@ import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; -import { ThemePalette } from '@koobiq/components/core'; -import { KbqDlModule } from '@koobiq/components/dl'; +import { DlOverviewExample } from 'packages/docs-examples/components/dl/dl-overview/dl-overview-example'; +import { DlResizableExample } from 'packages/docs-examples/components/dl/dl-resizable/dl-resizable-example'; +import { DevThemeToggle } from '../theme-toggle'; + +@Component({ + selector: 'dev-examples', + imports: [DlResizableExample, DlOverviewExample], + template: ` + +
+ +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DevDocsExamples {} @Component({ selector: 'dev-app', - imports: [KbqDlModule], + imports: [DevDocsExamples, DevThemeToggle], templateUrl: './template.html', styleUrl: './styles.scss', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }) -export class DevApp { - readonly themePalette = ThemePalette; -} +export class DevApp {} diff --git a/packages/components-dev/dl/styles.scss b/packages/components-dev/dl/styles.scss index 629401c69d..9083a14654 100644 --- a/packages/components-dev/dl/styles.scss +++ b/packages/components-dev/dl/styles.scss @@ -3,7 +3,3 @@ dev-app { max-width: 600px; padding: var(--kbq-size-s); } - -.kbq-dl { - border: 1px solid var(--kbq-palette-cyan-50); -} diff --git a/packages/components-dev/dl/template.html b/packages/components-dev/dl/template.html index 80ddd0878f..9eff7f844b 100644 --- a/packages/components-dev/dl/template.html +++ b/packages/components-dev/dl/template.html @@ -1,100 +1,5 @@ - - Тип инцидента - Вредоносное ПО + - Идентификатор - INC-2022-125-78253 +
- Статус - Новый - - Ответственный - Иванов Иван - - Описание - - Здесь нужно добавить очень длинное описание, но Я не знаю, что еще можно сюда добавить, поэтому Вы видите этот - текст. - -
- -
- - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - -
- - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - -
- - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - File - 125 КБ - - -
- - - Never show GUIDs - f3a9583e-346c-4a52-9ddb-252fd34ea9f0 - - -
- - - Description - - In order for your npm command line client to work with Artifactory you will firstly need to set the relevant - authentication. For getting authentication details run the following command - - - -
- - - Description - This should be rendered in vertical mode - + diff --git a/packages/components/core/locales/en-US.ts b/packages/components/core/locales/en-US.ts index 0d04ee509e..17a932a178 100644 --- a/packages/components/core/locales/en-US.ts +++ b/packages/components/core/locales/en-US.ts @@ -21,7 +21,8 @@ export const enUSLocaleData = { nextMonth: 'Next month', clear: 'Clear', showPassword: 'Show password', - hidePassword: 'Hide password' + hidePassword: 'Hide password', + resizeColumns: 'Resize columns' } satisfies KbqA11yLocaleConfiguration, select: { hiddenItemsText: '+{{ number }}', selectAll: 'Select all' } satisfies KbqSelectLocaleConfiguration, datepicker: { diff --git a/packages/components/core/locales/es-LA.ts b/packages/components/core/locales/es-LA.ts index 876878ff45..689fe168b3 100644 --- a/packages/components/core/locales/es-LA.ts +++ b/packages/components/core/locales/es-LA.ts @@ -21,7 +21,8 @@ export const esLALocaleData = { nextMonth: 'Mes siguiente', clear: 'Borrar', showPassword: 'Mostrar la contraseña', - hidePassword: 'Ocultar la contraseña' + hidePassword: 'Ocultar la contraseña', + resizeColumns: 'Redimensionar columnas' } satisfies KbqA11yLocaleConfiguration, select: { hiddenItemsText: '+{{ number }}', diff --git a/packages/components/core/locales/pt-BR.ts b/packages/components/core/locales/pt-BR.ts index 8cad608f86..9c88f8b9c4 100644 --- a/packages/components/core/locales/pt-BR.ts +++ b/packages/components/core/locales/pt-BR.ts @@ -21,7 +21,8 @@ export const ptBRLocaleData = { nextMonth: 'Próximo mês', clear: 'Apagar', showPassword: 'Mostrar a senha', - hidePassword: 'Ocultar a senha' + hidePassword: 'Ocultar a senha', + resizeColumns: 'Redimensionar colunas' } satisfies KbqA11yLocaleConfiguration, select: { hiddenItemsText: '+{{ number }}', diff --git a/packages/components/core/locales/ru-RU.ts b/packages/components/core/locales/ru-RU.ts index 39e7702567..ad0c6d1286 100644 --- a/packages/components/core/locales/ru-RU.ts +++ b/packages/components/core/locales/ru-RU.ts @@ -21,7 +21,8 @@ export const ruRULocaleData = { nextMonth: 'Следующий месяц', clear: 'Очистить', showPassword: 'Показать пароль', - hidePassword: 'Скрыть пароль' + hidePassword: 'Скрыть пароль', + resizeColumns: 'Изменить ширину колонок' } satisfies KbqA11yLocaleConfiguration, select: { hiddenItemsText: '+{{ number }}', selectAll: 'Выбрать все' } satisfies KbqSelectLocaleConfiguration, datepicker: { diff --git a/packages/components/core/locales/tk-TM.ts b/packages/components/core/locales/tk-TM.ts index d99ba46f63..638b5aaadb 100644 --- a/packages/components/core/locales/tk-TM.ts +++ b/packages/components/core/locales/tk-TM.ts @@ -21,7 +21,8 @@ export const tkTMLocaleData = { nextMonth: 'Indiki aý', clear: 'Arassala', showPassword: 'Paroly görkez', - hidePassword: 'Paroly gizle' + hidePassword: 'Paroly gizle', + resizeColumns: 'Sütünleriň giňligini üýtget' } satisfies KbqA11yLocaleConfiguration, select: { hiddenItemsText: '+{{ number }}', diff --git a/packages/components/core/locales/types.ts b/packages/components/core/locales/types.ts index 7af586899d..1dc921fdeb 100644 --- a/packages/components/core/locales/types.ts +++ b/packages/components/core/locales/types.ts @@ -29,6 +29,8 @@ export type KbqA11yLocaleConfiguration = { showPassword: string; /** Password form field button that masks the password. */ hidePassword: string; + /** Separator that resizes the columns of a description list. */ + resizeColumns: string; }; /** Locale configuration for `KbqCodeBlockModule`. */ diff --git a/packages/components/dl/dl-tokens.scss b/packages/components/dl/dl-tokens.scss index 002ca606e5..797858069b 100644 --- a/packages/components/dl/dl-tokens.scss +++ b/packages/components/dl/dl-tokens.scss @@ -3,7 +3,15 @@ --kbq-description-list-size-horizontal-gap-vertical: var(--kbq-size-l); --kbq-description-list-size-vertical-content-gap-vertical: var(--kbq-size-3xs); --kbq-description-list-size-vertical-gap-vertical: var(--kbq-size-m); + --kbq-description-list-resizer-line-width: 1px; + --kbq-description-list-resizer-hit-area-width: calc( + 2 * var(--kbq-size-s) + var(--kbq-description-list-resizer-line-width) + ); + --kbq-description-list-resizer-transition-duration: 0.2s; /* THEME TOKENS */ --kbq-description-list-term-color: var(--kbq-foreground-contrast-secondary); --kbq-description-list-description-color: var(--kbq-foreground-contrast); + --kbq-description-list-resizer-color: var(--kbq-line-contrast-less); + --kbq-description-list-resizer-state-hover-color: var(--kbq-line-contrast-fade); + --kbq-description-list-resizer-state-active-color: var(--kbq-states-line-focus-theme); } diff --git a/packages/components/dl/dl.component.spec.ts b/packages/components/dl/dl.component.spec.ts index a81e62c62f..34558df6e1 100644 --- a/packages/components/dl/dl.component.spec.ts +++ b/packages/components/dl/dl.component.spec.ts @@ -1,9 +1,26 @@ -import { Provider, Type } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Directionality } from '@angular/cdk/bidi'; +import { SharedResizeObserver } from '@angular/cdk/observers/private'; +import { Injectable, Provider, Type } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { enUSLocaleData, kbqA11yLocaleConfigurationProvider } from '@koobiq/components/core'; +import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; import { KbqDlAlign, KbqDlComponent } from './dl.component'; +/** `SharedResizeObserver` stand-in: the real one never emits in jsdom, where `ResizeObserver` is a no-op stub. */ +@Injectable() +class MockResizeObserver extends SharedResizeObserver { + changes = new BehaviorSubject([]); + + override observe(_target: Element, _options?: ResizeObserverOptions): Observable { + return this.changes.asObservable(); + } +} + const createComponent = (component: Type, providers: Provider[] = []): ComponentFixture => { - TestBed.configureTestingModule({ imports: [component], providers }); + TestBed.configureTestingModule({ + imports: [component], + providers: [{ provide: SharedResizeObserver, useClass: MockResizeObserver }, ...providers] + }); const fixture = TestBed.createComponent(component); fixture.autoDetectChanges(); @@ -12,6 +29,9 @@ const createComponent = (component: Type, providers: Provider[] = []): Com }; const getDlElement = ({ nativeElement }: ComponentFixture): HTMLElement => nativeElement; +const getResizeHandle = ({ nativeElement }: ComponentFixture): HTMLElement | null => { + return nativeElement.querySelector('.kbq-dl__resize-handle'); +}; describe(KbqDlComponent.name, () => { it('should use start alignment by default', () => { @@ -56,4 +76,337 @@ describe(KbqDlComponent.name, () => { expect(getDlElement(fixture).classList).toContain('kbq-dl_wide'); expect(getDlElement(fixture).classList).toContain('kbq-dl_vertical'); }); + + it('should render resize handle only when resizing is enabled for horizontal layout', () => { + const fixture = createComponent(KbqDlComponent); + + expect(getResizeHandle(fixture)).toBeNull(); + + fixture.componentRef.setInput('columnResizable', true); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)).not.toBeNull(); + + fixture.componentRef.setInput('vertical', true); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)).toBeNull(); + }); + + it('should constrain the resize track to the configured widths once a width is set', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('columnMinWidth', 120); + fixture.componentRef.setInput('remainingColumnMinWidth', 200); + fixture.componentRef.setInput('columnWidth', 300); + Object.defineProperty(getDlElement(fixture), 'clientWidth', { configurable: true, value: 600 }); + fixture.detectChanges(); + + const resizeTrack = getDlElement(fixture).querySelector('.kbq-dl__resize-track')!; + + expect(resizeTrack.style.width).toBe('300px'); + expect(resizeTrack.style.minWidth).toBe('120px'); + expect(resizeTrack.style.maxWidth).toBe('400px'); + }); + + it('should expose the column min width as a CSS variable so the resting grid honors it', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('columnMinWidth', 200); + fixture.detectChanges(); + + expect(getDlElement(fixture).style.getPropertyValue('--kbq-description-list-column-min-width')).toBe('200px'); + }); + + it('should not constrain the resting track before a width is set, so the separator stays on the column border', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + // A min width wider than the natural column must not inflate the resting track into the second column. + fixture.componentRef.setInput('columnMinWidth', 200); + fixture.componentRef.setInput('remainingColumnMinWidth', 200); + Object.defineProperty(getDlElement(fixture), 'clientWidth', { configurable: true, value: 600 }); + fixture.detectChanges(); + + const resizeTrack = getDlElement(fixture).querySelector('.kbq-dl__resize-track')!; + + expect(resizeTrack.style.width).toBe(''); + expect(resizeTrack.style.minWidth).toBe(''); + expect(resizeTrack.style.maxWidth).toBe(''); + }); + + it('should resize the first column with keyboard and expose separator semantics', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('columnMinWidth', 96); + fixture.detectChanges(); + + const resizeHandle = getResizeHandle(fixture)!; + + resizeHandle.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + fixture.detectChanges(); + + // The configured min width (96) is also the max here (host width is 0 in jsdom), so it clamps to 96. + expect(fixture.componentInstance.columnWidth()).toBe(96); + expect(resizeHandle.getAttribute('role')).toBe('separator'); + expect(resizeHandle.getAttribute('aria-orientation')).toBe('vertical'); + expect(getDlElement(fixture).classList).toContain('kbq-dl_resized'); + }); + + it('should collapse to the minimum width on double click, then reset on the next one', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('columnMinWidth', 120); + fixture.componentRef.setInput('columnWidth', 200); + Object.defineProperty(getDlElement(fixture), 'clientWidth', { configurable: true, value: 600 }); + fixture.detectChanges(); + + const resizeHandle = getResizeHandle(fixture)!; + + resizeHandle.dispatchEvent(new MouseEvent('dblclick')); + fixture.detectChanges(); + + expect(fixture.componentInstance.columnWidth()).toBe(120); + expect(getDlElement(fixture).classList).toContain('kbq-dl_resized'); + + resizeHandle.dispatchEvent(new MouseEvent('dblclick')); + fixture.detectChanges(); + + expect(fixture.componentInstance.columnWidth()).toBeNull(); + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_resized'); + }); + + it('should update the shared column width on pointer drag', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.detectChanges(); + + const resizeTrack = getDlElement(fixture).querySelector('.kbq-dl__resize-track')!; + const resizeHandle = getResizeHandle(fixture)!; + + Object.defineProperty(getDlElement(fixture), 'clientWidth', { configurable: true, value: 600 }); + // The resizer captures the base size via `getResizableSize()`; force its border-box branch to report 120. + resizeTrack.style.boxSizing = 'border-box'; + Object.defineProperty(resizeTrack, 'getBoundingClientRect', { + configurable: true, + value: () => ({ width: 120, height: 0 }) as DOMRect + }); + + resizeHandle.dispatchEvent(new MouseEvent('pointerdown', { clientX: 120 })); + document.dispatchEvent(new MouseEvent('pointermove', { buttons: 1, clientX: 200 })); + fixture.detectChanges(); + + expect(fixture.componentInstance.columnWidth()).toBe(200); + }); + + it('should reverse horizontal resize direction in RTL', () => { + const fixture = createComponent(KbqDlComponent, [ + { provide: Directionality, useValue: { value: 'rtl', change: EMPTY } } + ]); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.detectChanges(); + + const resizeTrack = getDlElement(fixture).querySelector('.kbq-dl__resize-track')!; + + Object.defineProperty(getDlElement(fixture), 'clientWidth', { configurable: true, value: 600 }); + Object.defineProperty(resizeTrack, 'clientWidth', { configurable: true, value: 200 }); + + getResizeHandle(fixture)!.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + fixture.detectChanges(); + + expect(fixture.componentInstance.columnWidth()).toBe(208); + }); + + it('should apply the col-resize cursor to the separator', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)!.style.cursor).toBe('col-resize'); + }); + + it('should fall back to the localized separator name for the aria-label', () => { + const fixture = createComponent(KbqDlComponent, [ + kbqA11yLocaleConfigurationProvider({ ...enUSLocaleData.a11y, resizeColumns: 'Resize the columns' }) + ]); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)!.getAttribute('aria-label')).toBe('Resize the columns'); + }); + + it('should prefer the provided resizer aria-label over the localized default', () => { + const fixture = createComponent(KbqDlComponent, [ + kbqA11yLocaleConfigurationProvider({ ...enUSLocaleData.a11y, resizeColumns: 'Localized default' }) + ]); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('resizerAriaLabel', 'Custom label'); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)!.getAttribute('aria-label')).toBe('Custom label'); + }); + + it('should clamp a negative column min width to zero', () => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + fixture.componentRef.setInput('vertical', false); + fixture.componentRef.setInput('columnMinWidth', -50); + fixture.detectChanges(); + + expect(getResizeHandle(fixture)!.getAttribute('aria-valuemin')).toBe('0'); + }); + + it('should auto-detect the vertical layout from the host width via the resize observer', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + // The layout decision reads `getClientRects()`; report a width below the default `verticalBreakpoint` (400). + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 320 } as DOMRect] + }); + + // Flush the resize-observer debounce so the initial `startWith(null)` emission runs `updateLayout`. + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).toContain('kbq-dl_vertical'); + expect(getResizeHandle(fixture)).toBeNull(); + flush(); + })); + + it('should keep the horizontal layout when the host is wider than verticalBreakpoint', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('columnResizable', true); + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 800 } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_vertical'); + expect(getResizeHandle(fixture)).not.toBeNull(); + flush(); + })); + + it('should let an explicit vertical input override the auto-detection', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + // Host is narrow (auto-detection would choose vertical), but the explicit input forces horizontal. + fixture.componentRef.setInput('vertical', false); + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 200 } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_vertical'); + flush(); + })); + + it('should re-evaluate the layout against verticalBreakpoint on every resize while in auto mode', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + const observer = TestBed.inject(SharedResizeObserver) as MockResizeObserver; + + let width = 800; + + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_vertical'); + + // Shrink below the threshold: the layout must switch to vertical, not stay locked to the first measurement. + width = 300; + observer.changes.next([]); + tick(100); + fixture.detectChanges(); + expect(getDlElement(fixture).classList).toContain('kbq-dl_vertical'); + + // Grow back above the threshold: it must switch back to horizontal. + width = 800; + observer.changes.next([]); + tick(100); + fixture.detectChanges(); + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_vertical'); + flush(); + })); + + it('should honor a custom verticalBreakpoint as the vertical-layout threshold', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + // 600px is above the default threshold (400) but below the custom one (700), so only a working + // `verticalBreakpoint` input makes the layout switch to vertical here. + fixture.componentRef.setInput('verticalBreakpoint', 700); + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 600 } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).toContain('kbq-dl_vertical'); + flush(); + })); + + it('should stay horizontal when the host is wider than a custom verticalBreakpoint', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + fixture.componentRef.setInput('verticalBreakpoint', 500); + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 600 } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).not.toContain('kbq-dl_vertical'); + flush(); + })); + + it('should still honor the deprecated minWidth alias as the vertical-layout threshold', fakeAsync(() => { + const fixture = createComponent(KbqDlComponent); + + // 600px is above the default (400) but below the alias value, so only a working `minWidth` makes it vertical. + fixture.componentRef.setInput('minWidth', 700); + Object.defineProperty(getDlElement(fixture), 'getClientRects', { + configurable: true, + value: () => [{ width: 600 } as DOMRect] + }); + + tick(100); + fixture.detectChanges(); + + expect(getDlElement(fixture).classList).toContain('kbq-dl_vertical'); + flush(); + })); }); diff --git a/packages/components/dl/dl.component.ts b/packages/components/dl/dl.component.ts index e9b477c886..4e699447cc 100644 --- a/packages/components/dl/dl.component.ts +++ b/packages/components/dl/dl.component.ts @@ -1,97 +1,293 @@ +import { Direction, Directionality } from '@angular/cdk/bidi'; +import { SharedResizeObserver } from '@angular/cdk/observers/private'; import { Platform } from '@angular/cdk/platform'; import { - AfterContentInit, + afterNextRender, + booleanAttribute, + ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, + contentChildren, + DestroyRef, ElementRef, inject, - Input, input, - OnDestroy, + model, + numberAttribute, + signal, + viewChild, ViewEncapsulation } from '@angular/core'; -import { Subject, Subscription } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { KBQ_WINDOW, kbqInjectA11yLocaleConfiguration, kbqInjectNativeElement } from '@koobiq/components/core'; +import { KbqResizable, KbqResizer, KbqResizerDirection, KbqResizerSizeChangeEvent } from '@koobiq/components/resizer'; import { debounceTime, startWith } from 'rxjs/operators'; /** Supported alignment values for description list items. */ export type KbqDlAlign = 'start' | 'center' | 'end'; @Component({ - selector: 'kbq-dl', + selector: 'kbq-dt', + template: '', + encapsulation: ViewEncapsulation.None, + host: { + class: 'kbq-dt' + } +}) +export class KbqDtComponent {} + +@Component({ + selector: 'kbq-dd', template: '', + encapsulation: ViewEncapsulation.None, + host: { + class: 'kbq-dd' + } +}) +export class KbqDdComponent {} + +@Component({ + selector: 'kbq-dl', + imports: [KbqResizable, KbqResizer], + template: ` + + + @if (resizerVisible()) { +
+ +
+ } + `, styleUrls: ['dl.scss', 'dl-tokens.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'kbq-dl', - '[class.kbq-dl_vertical]': 'vertical', + '[class.kbq-dl_vertical]': 'isVertical()', '[class.kbq-dl_wide]': 'wide()', + '[class.kbq-dl_resizable]': 'resizerVisible()', + '[class.kbq-dl_resized]': 'columnWidth() !== null', '[class.kbq-dl_vertical-align-center]': "verticalAlign() === 'center'", '[class.kbq-dl_vertical-align-end]': "verticalAlign() === 'end'", '[class.kbq-dl_horizontal-align-center]': "horizontalAlign() === 'center'", '[class.kbq-dl_horizontal-align-end]': "horizontalAlign() === 'end'", - '(window:resize)': 'resizeStream.next($event)' + '[style.--kbq-description-list-column-width.px]': 'columnWidth()', + '[style.--kbq-description-list-column-min-width.px]': 'normalizedColumnMinWidth()' } }) -export class KbqDlComponent implements AfterContentInit, OnDestroy { - readonly minWidth = input(400); +export class KbqDlComponent { + /** Host width in pixels at or below which the list auto-switches to the vertical layout. */ + readonly verticalBreakpoint = input(400, { transform: numberAttribute }); + + /** + * Host width in pixels at or below which the list auto-switches to the vertical layout. + * @deprecated The name is misleading (it is a breakpoint, not a min width). Use `verticalBreakpoint` instead. + * Will be removed in a future major release. When both are set, `minWidth` takes precedence. + */ + readonly minWidth = input(); + + /** Whether the list uses the wide two-column layout. */ readonly wide = input(false); + + /** Whether the columns can be resized by dragging the separator between them. */ + readonly columnResizable = input(false, { transform: booleanAttribute }); + + /** Current width of the first (resizable) column in pixels; `null` restores the default column ratio. */ + readonly columnWidth = model(null); + + /** Minimum width of the first (resizable) column in pixels; defaults to the term width measured after render. */ + readonly columnMinWidth = input(undefined); + + /** Minimum width retained for the remaining column in pixels; defaults to the term width measured after render. */ + readonly remainingColumnMinWidth = input(undefined); + + /** Accessible name of the column resize separator; falls back to the localized default when omitted. */ + readonly resizerAriaLabel = input(undefined); + /** Vertical alignment of `kbq-dt` and `kbq-dd` items. */ readonly verticalAlign = input('start'); + /** Horizontal alignment of `kbq-dt` and `kbq-dd` items. */ readonly horizontalAlign = input('start'); - // TODO: Skipped for migration because: - // Your application code writes to the input. This prevents migration. - @Input() vertical: boolean | null = null; - readonly resizeStream = new Subject(); - private readonly resizeDebounceInterval: number = 100; + /** Forces the vertical layout; `null` lets the list decide based on `verticalBreakpoint`. */ + readonly vertical = input(null); + + /** @docs-private */ + protected readonly resizeDirection = signal([1, 0]); + + /** Auto-detected vertical layout, re-evaluated from the host width on resize while `vertical` is not set. */ + private readonly autoVertical = signal(null); + + /** @docs-private Effective vertical layout, combining the explicit `vertical` input and the auto-detection. */ + protected readonly isVertical = computed(() => this.vertical() ?? this.autoVertical() ?? false); + + /** @docs-private Whether the column resize separator is currently rendered. */ + protected readonly resizerVisible = computed(() => this.columnResizable() && !this.isVertical()); + + /** Term column width measured after the first render; the default minimum for both columns when unset. */ + private readonly measuredColumnWidth = signal(0); + + /** Effective minimum width of the first column: the input, or the measured term width when not provided. */ + private readonly resolvedColumnMinWidth = computed(() => this.columnMinWidth() ?? this.measuredColumnWidth()); + + /** Effective minimum width of the remaining column: the input, or the measured term width when not provided. */ + private readonly resolvedRemainingColumnMinWidth = computed( + () => this.remainingColumnMinWidth() ?? this.measuredColumnWidth() + ); + + /** @docs-private Term column minimum width, clamped to a non-negative value. */ + protected readonly normalizedColumnMinWidth = computed(() => Math.max(0, this.resolvedColumnMinWidth())); + + protected get maxColumnWidth(): number { + if (!this.platform.isBrowser) return this.normalizedColumnMinWidth(); + + const hostWidth = this.nativeElement.clientWidth; + const columnGap = parseFloat(this.window.getComputedStyle(this.nativeElement).columnGap) || 0; + + return Math.max( + this.normalizedColumnMinWidth(), + hostWidth - columnGap - Math.max(0, this.resolvedRemainingColumnMinWidth()) + ); + } - private resizeSubscription = Subscription.EMPTY; + protected get currentColumnWidth(): number { + if (!this.platform.isBrowser) return this.normalizedColumnMinWidth(); - private readonly elementRef = inject>(ElementRef); + return Math.round(this.resizeTrack()?.nativeElement.clientWidth || this.normalizedColumnMinWidth()); + } + + private readonly resizeDebounceInterval: number = 100; + private readonly keyboardResizeStep: number = 8; + + private readonly nativeElement = kbqInjectNativeElement(); private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly platform = inject(Platform); + private readonly window = inject(KBQ_WINDOW); + private readonly destroyRef = inject(DestroyRef); + private readonly resizeObserver = inject(SharedResizeObserver); + private readonly directionality = inject(Directionality, { optional: true }); + private readonly a11yLocaleConfiguration = kbqInjectA11yLocaleConfiguration(); + private readonly resizeTrack = viewChild>('resizeTrack'); + private readonly terms = contentChildren(KbqDtComponent, { read: ElementRef }); + + /** @docs-private Resolved accessible name of the resize separator. */ + protected readonly resolvedResizerAriaLabel = computed( + () => this.resizerAriaLabel() || this.a11yLocaleConfiguration().resizeColumns + ); + + constructor() { + this.updateResizeDirection(this.directionality?.value); + this.directionality?.change.pipe(takeUntilDestroyed()).subscribe((direction) => { + this.updateResizeDirection(direction); + }); + + // `afterNextRender` runs in the browser only, so no explicit platform guard is needed here. + afterNextRender(() => { + this.measureColumnWidth(); + + this.resizeObserver + .observe(this.nativeElement) + .pipe(startWith(null), debounceTime(this.resizeDebounceInterval), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.updateLayout()); + }); + } + + /** Captures the rendered width of the term column, used as the default minimum when no widths are provided. */ + private measureColumnWidth(): void { + const term = this.terms()[0]?.nativeElement; + + if (term) this.measuredColumnWidth.set(Math.round(term.getBoundingClientRect().width)); + } + + /** @docs-private */ + protected handleColumnResize({ width }: KbqResizerSizeChangeEvent): void { + this.setColumnWidth(width); + } - ngAfterContentInit(): void { - if (this.vertical !== null || !this.platform.isBrowser) { - return; + /** @docs-private */ + protected handleResizeKeydown(event: KeyboardEvent): void { + let width: number | null = null; + + if (event.key === 'Home') width = this.normalizedColumnMinWidth(); + if (event.key === 'End') width = this.maxColumnWidth; + + if (event.key === 'ArrowLeft') { + width = this.currentColumnWidth - this.resizeDirection()[0] * this.keyboardResizeStep; } - this.resizeSubscription = this.resizeStream - .pipe(startWith(null), debounceTime(this.resizeDebounceInterval)) - .subscribe(this.updateState); + if (event.key === 'ArrowRight') { + width = this.currentColumnWidth + this.resizeDirection()[0] * this.keyboardResizeStep; + } + + if (width === null) return; + + event.preventDefault(); + this.setColumnWidth(width); } - ngOnDestroy() { - this.resizeSubscription.unsubscribe(); + /** @docs-private */ + protected handleResizeDblClick(event: MouseEvent): void { + event.preventDefault(); + + // First double-click collapses the first column to its minimum width; a second one restores the default ratio. + if (this.columnWidth() === this.normalizedColumnMinWidth()) { + this.columnWidth.set(null); + } else { + this.setColumnWidth(this.normalizedColumnMinWidth()); + } } - private readonly updateState = (): void => { - const domRect = this.elementRef.nativeElement.getClientRects()[0]; - const width = domRect?.width || 0; + private updateLayout(): void { + // While `vertical` is not set explicitly, re-evaluate the layout against the breakpoint on every resize. + if (this.vertical() === null) { + const domRect = this.nativeElement.getClientRects()[0]; + const width = domRect?.width || 0; + // `minWidth` is the deprecated alias of `verticalBreakpoint`; honor it when a consumer still sets it. + const breakpoint = this.minWidth() ?? this.verticalBreakpoint(); + + this.autoVertical.set(width <= breakpoint); + } - this.vertical = width <= this.minWidth(); + if (this.columnWidth() !== null && !this.isVertical()) this.setColumnWidth(this.columnWidth()!); + // Signal writes above already schedule change detection, but `maxColumnWidth` and `currentColumnWidth` read + // live layout geometry rather than signals: a resize changes their values without touching any signal, so the + // OnPush view must be told to re-check for the `aria-valuemax`/`aria-valuenow`/track-size bindings to refresh. this.changeDetectorRef.markForCheck(); - }; -} + } -@Component({ - selector: 'kbq-dt', - template: '', - encapsulation: ViewEncapsulation.None, - host: { - class: 'kbq-dt' + private setColumnWidth(width: number): void { + if (!Number.isFinite(width)) return; + + const constrainedWidth = Math.min(this.maxColumnWidth, Math.max(this.normalizedColumnMinWidth(), width)); + + if (constrainedWidth !== this.columnWidth()) this.columnWidth.set(constrainedWidth); } -}) -export class KbqDtComponent {} -@Component({ - selector: 'kbq-dd', - template: '', - encapsulation: ViewEncapsulation.None, - host: { - class: 'kbq-dd' + private updateResizeDirection(direction: Direction | undefined): void { + this.resizeDirection.set(direction === 'rtl' ? [-1, 0] : [1, 0]); } -}) -export class KbqDdComponent {} +} diff --git a/packages/components/dl/dl.en.md b/packages/components/dl/dl.en.md index ea569c267c..ecaa9d3100 100644 --- a/packages/components/dl/dl.en.md +++ b/packages/components/dl/dl.en.md @@ -2,6 +2,12 @@ +### Resizable + +Enabled with the `columnResizable` flag. A separator appears on the column border: drag it with the mouse or the keyboard (arrows, `Home`/`End`); a double-click collapses the first column to its minimum, a second one restores the default layout. Control the width via `[(columnWidth)]`, and set the bounds with `columnMinWidth` and `remainingColumnMinWidth`. + + + ### Small (adaptive) diff --git a/packages/components/dl/dl.ru.md b/packages/components/dl/dl.ru.md index ea569c267c..c7f6f2f138 100644 --- a/packages/components/dl/dl.ru.md +++ b/packages/components/dl/dl.ru.md @@ -1,15 +1,21 @@ -### Default (adaptive) +### По умолчанию (адаптивный) -### Small (adaptive) +### Изменение ширины колонок + +Включается флагом `columnResizable`. На границе колонок появляется разделитель: тяните его мышью или клавишами (стрелки, `Home`/`End`); двойной клик сворачивает первую колонку до минимума, повторный — сбрасывает к раскладке по умолчанию. Ширину можно контролировать через `[(columnWidth)]`, а границы задать через `columnMinWidth` и `remainingColumnMinWidth`. + + + +### Компактный (адаптивный) -### Horizontal +### Горизонтальный -### Vertical +### Вертикальный diff --git a/packages/components/dl/dl.scss b/packages/components/dl/dl.scss index dbba61c004..ca196d7e5b 100644 --- a/packages/components/dl/dl.scss +++ b/packages/components/dl/dl.scss @@ -1,7 +1,8 @@ -@use './dl-theme' as *; +@use './dl-theme' as theme; .kbq-dl { display: grid; + position: relative; column-gap: var(--kbq-description-list-size-horizontal-content-gap-horizontal); row-gap: var(--kbq-description-list-size-horizontal-gap-vertical); @@ -31,9 +32,91 @@ grid-column: 2 / span 3; } + // Before the first resize, keep the term column at least as wide as its configured minimum so the separator + // rests on the real column border and dragging starts from a width that already satisfies the minimum. + &.kbq-dl_resizable:not(.kbq-dl_resized):not(.kbq-dl_vertical) { + grid-template-columns: + minmax(var(--kbq-description-list-column-min-width), 1fr) + repeat(3, minmax(0, 1fr)); + } + + &.kbq-dl_resized:not(.kbq-dl_vertical) { + grid-template-columns: + var(--kbq-description-list-column-width) + repeat(3, minmax(0, 1fr)); + } + + .kbq-dl__resize-track { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + grid-column: 1 / 2; + width: 100%; + pointer-events: none; + z-index: 1; + } + + .kbq-dl__resize-handle { + position: absolute; + inset-block: 0; + inset-inline-end: calc( + ( + var(--kbq-description-list-size-horizontal-content-gap-horizontal) + + var(--kbq-description-list-resizer-hit-area-width) + ) / + -2 + ); + width: var(--kbq-description-list-resizer-hit-area-width); + pointer-events: auto; + touch-action: none; + + // The resize line, centered inside the hit area. Hidden until the list is hovered or the separator is focused. + &::after { + position: absolute; + inset-block: 0; + inset-inline: 0; + width: var(--kbq-description-list-resizer-line-width); + margin-inline: auto; + background-color: var(--kbq-description-list-resizer-color); + content: ''; + opacity: 0; + transition: + opacity var(--kbq-description-list-resizer-transition-duration) ease-in-out, + background-color var(--kbq-description-list-resizer-transition-duration) ease-in-out; + } + + &:hover::after { + background-color: var(--kbq-description-list-resizer-state-hover-color); + } + + &:focus-visible::after, + &:active::after { + background-color: var(--kbq-description-list-resizer-state-active-color); + } + + // Keep the separator visible while it is focused or being dragged, even without a pointer hover. + &:focus-visible::after, + &:active::after { + opacity: 1; + } + } + + // Reveal the separator on hover of the list. + &:hover .kbq-dl__resize-handle::after { + opacity: 1; + } + &.kbq-dl_wide { grid-template-columns: repeat(2, 1fr); + &.kbq-dl_resizable:not(.kbq-dl_resized):not(.kbq-dl_vertical) { + grid-template-columns: minmax(var(--kbq-description-list-column-min-width), 1fr) minmax(0, 1fr); + } + + &.kbq-dl_resized:not(.kbq-dl_vertical) { + grid-template-columns: var(--kbq-description-list-column-width) minmax(0, 1fr); + } + .kbq-dt { grid-column: 1; } @@ -59,5 +142,5 @@ } } -@include kbq-dl-theme(); -@include kbq-dl-typography(); +@include theme.kbq-dl-theme(); +@include theme.kbq-dl-typography(); diff --git a/packages/components/dl/e2e.playwright-spec.ts b/packages/components/dl/e2e.playwright-spec.ts index 06156b1efb..2f3ea0ac12 100644 --- a/packages/components/dl/e2e.playwright-spec.ts +++ b/packages/components/dl/e2e.playwright-spec.ts @@ -1,4 +1,4 @@ -import { expect, Page, test } from '@playwright/test'; +import { expect, Locator, Page, test } from '@playwright/test'; import { e2eEnableDarkTheme } from '../../e2e/utils'; test.describe('KbqDlModule', () => { @@ -12,4 +12,86 @@ test.describe('KbqDlModule', () => { await expect(getComponent(page)).toHaveScreenshot('01-dark.png'); }); }); + + test.describe('E2eDlResizable', () => { + const getComponent = (page: Page) => page.getByTestId('e2eDlResizableConfigured'); + const getAutoComponent = (page: Page) => page.getByTestId('e2eDlResizableAuto'); + const getList = (block: Locator) => block.locator('.kbq-dl'); + const getSeparator = (block: Locator) => block.getByRole('separator'); + const getFirstTerm = (block: Locator) => block.locator('.kbq-dt').first(); + + const resizedClass = /(?:^|\s)kbq-dl_resized(?:\s|$)/; + + test('should widen the first column when the separator is dragged to the right', async ({ page }) => { + await page.goto('/E2eDlResizable'); + + const block = getComponent(page); + const separator = getSeparator(block); + const term = getFirstTerm(block); + + const startWidth = (await term.boundingBox())!.width; + + await separator.hover(); + const box = (await separator.boundingBox())!; + const startX = box.x + box.width / 2; + const startY = box.y + box.height / 2; + const dragOffset = 60; + + await page.mouse.down(); + await page.mouse.move(startX + dragOffset, startY, { steps: 5 }); + await page.mouse.up(); + + const endWidth = (await term.boundingBox())!.width; + + expect(endWidth).toBeGreaterThan(startWidth); + expect(endWidth).toBeCloseTo(startWidth + dragOffset, 0); + await expect(getList(block)).toHaveClass(resizedClass); + }); + + test('should collapse to the minimum on double click and reset on the second', async ({ page }) => { + await page.goto('/E2eDlResizable'); + + const block = getComponent(page); + const separator = getSeparator(block); + + await separator.dblclick(); + await expect(getList(block)).toHaveClass(resizedClass); + await expect(separator).toHaveAttribute('aria-valuenow', '120'); + + await separator.dblclick(); + await expect(getList(block)).not.toHaveClass(resizedClass); + }); + + test('should resize the first column with the keyboard', async ({ page }) => { + await page.goto('/E2eDlResizable'); + + const block = getComponent(page); + const separator = getSeparator(block); + const term = getFirstTerm(block); + + const startWidth = (await term.boundingBox())!.width; + + await separator.focus(); + await page.keyboard.press('ArrowRight'); + + await expect(getList(block)).toHaveClass(resizedClass); + expect((await term.boundingBox())!.width).toBeGreaterThanOrEqual(startWidth); + }); + + test('should default the column minimums to the measured term width when they are not set', async ({ + page + }) => { + await page.goto('/E2eDlResizable'); + + const block = getAutoComponent(page); + const separator = getSeparator(block); + const termWidth = Math.round((await getFirstTerm(block).boundingBox())!.width); + + // With no `columnMinWidth`/`remainingColumnMinWidth`, the resizer falls back to the measured term width. + const ariaValueMin = Number(await separator.getAttribute('aria-valuemin')); + + expect(ariaValueMin).toBeGreaterThan(0); + expect(ariaValueMin).toBeCloseTo(termWidth, 0); + }); + }); }); diff --git a/packages/components/dl/e2e.ts b/packages/components/dl/e2e.ts index 2804c49564..5d5fe8629f 100644 --- a/packages/components/dl/e2e.ts +++ b/packages/components/dl/e2e.ts @@ -8,7 +8,7 @@ import { KbqDlModule } from './dl.module'; template: `
- + dt dd dt @@ -22,7 +22,7 @@ import { KbqDlModule } from './dl.module';
- + dt dd dt @@ -36,7 +36,7 @@ import { KbqDlModule } from './dl.module';
- + center Badge @@ -48,7 +48,7 @@ import { KbqDlModule } from './dl.module';
- + Multiline description term end end @@ -58,7 +58,7 @@ import { KbqDlModule } from './dl.module';
- + center dd center @@ -68,7 +68,7 @@ import { KbqDlModule } from './dl.module';
- + end definition end @@ -112,3 +112,43 @@ import { KbqDlModule } from './dl.module'; } }) export class E2eDlStates {} + +@Component({ + selector: 'e2e-dl-resizable', + imports: [KbqDlModule], + template: ` +
+ + Incident type + Malware infection + Identifier + INC-2026-125-78253 + Description + A long description that keeps the second column filled while the first one is resized. + +
+ +
+ + Incident type + Malware infection + Identifier + INC-2026-125-78253 + Description + A long description that keeps the second column filled while the first one is resized. + +
+ `, + styles: ` + :host { + display: block; + width: 600px; + padding: var(--kbq-size-m); + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'data-testid': 'e2eDlResizable' + } +}) +export class E2eDlResizable {} diff --git a/packages/components/resizer/resizer.spec.ts b/packages/components/resizer/resizer.spec.ts index f44aa5aed6..4b09e3da2a 100644 --- a/packages/components/resizer/resizer.spec.ts +++ b/packages/components/resizer/resizer.spec.ts @@ -51,7 +51,7 @@ const windowStub = (boxSizing: 'content-box' | 'border-box'): Provider => ({ standalone: true, template: `
-
+
` }) @@ -60,6 +60,7 @@ export class TestResizer { readonly resizable = viewChild.required(KbqResizable); readonly direction = signal([1, 0]); + readonly cursor = signal(null); readonly sizeChange = jest.fn(); } @@ -84,6 +85,31 @@ describe(KbqResizer.name, () => { expect(getResizerElement(fixture).style.cursor).toBe(cursor); }); + it('should override the direction-derived cursor with the `cursor` input', () => { + const fixture = createComponent(TestResizer); + + fixture.componentInstance.direction.set([1, 0]); + fixture.componentInstance.cursor.set('col-resize'); + fixture.detectChanges(); + + expect(getResizerElement(fixture).style.cursor).toBe('col-resize'); + }); + + it('should fall back to the direction-derived cursor when the `cursor` input is empty', () => { + const fixture = createComponent(TestResizer); + + fixture.componentInstance.direction.set([0, 1]); + fixture.componentInstance.cursor.set('col-resize'); + fixture.detectChanges(); + + expect(getResizerElement(fixture).style.cursor).toBe('col-resize'); + + fixture.componentInstance.cursor.set(null); + fixture.detectChanges(); + + expect(getResizerElement(fixture).style.cursor).toBe('ns-resize'); + }); + it('should emit sizeChange event when resizing', async () => { const fixture = createComponent(TestResizer); diff --git a/packages/components/resizer/resizer.ts b/packages/components/resizer/resizer.ts index c69c3e7461..c5d9262532 100644 --- a/packages/components/resizer/resizer.ts +++ b/packages/components/resizer/resizer.ts @@ -70,7 +70,7 @@ export class KbqResizable { selector: '[kbqResizer]', host: { class: 'kbq-resizer', - '[style.cursor]': 'cursor()' + '[style.cursor]': 'resolvedCursor()' }, exportAs: 'kbqResizer' }) @@ -94,6 +94,11 @@ export class KbqResizer { */ readonly direction = input.required({ alias: 'kbqResizer' }); + /** + * Overrides the cursor that is otherwise derived from the resizing direction. + */ + readonly cursor = input(null); + /** * Emits the new size of the element after resizing. */ @@ -102,7 +107,11 @@ export class KbqResizer { /** * @docs-private */ - protected readonly cursor = computed(() => { + protected readonly resolvedCursor = computed(() => { + const cursor = this.cursor(); + + if (cursor) return cursor; + const [x, y] = this.direction(); if (x === 0 && y === 0) return 'default'; diff --git a/packages/docs-examples/components/dl/dl-resizable/dl-resizable-example.ts b/packages/docs-examples/components/dl/dl-resizable/dl-resizable-example.ts new file mode 100644 index 0000000000..28376aae52 --- /dev/null +++ b/packages/docs-examples/components/dl/dl-resizable/dl-resizable-example.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { KbqBadgeModule } from '@koobiq/components/badge'; +import { KbqDlModule } from '@koobiq/components/dl'; + +/** + * @title Description list resizable example + */ +@Component({ + selector: 'dl-resizable-example', + imports: [KbqDlModule, KbqBadgeModule], + template: ` + + Incident type + + DDoS + + Identifier + INC-2026-125-78253 + Description + + In a distributed denial-of-service attack (DDoS attack), the incoming traffic flooding the victim + originates from many different sources. More sophisticated strategies are required to mitigate this type + of attack; simply attempting to block a single source is insufficient as there are multiple sources. + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DlResizableExample {} diff --git a/packages/docs-examples/components/dl/index.ts b/packages/docs-examples/components/dl/index.ts index 78941140a7..49cfe7c087 100644 --- a/packages/docs-examples/components/dl/index.ts +++ b/packages/docs-examples/components/dl/index.ts @@ -1,16 +1,24 @@ import { NgModule } from '@angular/core'; import { DlHorizontalOverviewExample } from './dl-horizontal-overview/dl-horizontal-overview-example'; import { DlOverviewExample } from './dl-overview/dl-overview-example'; +import { DlResizableExample } from './dl-resizable/dl-resizable-example'; import { DlSmallExample } from './dl-small/dl-small-example'; import { DlVerticalOverviewExample } from './dl-vertical-overview/dl-vertical-overview-example'; -export { DlHorizontalOverviewExample, DlOverviewExample, DlSmallExample, DlVerticalOverviewExample }; +export { + DlHorizontalOverviewExample, + DlOverviewExample, + DlResizableExample, + DlSmallExample, + DlVerticalOverviewExample +}; const EXAMPLES = [ DlOverviewExample, DlHorizontalOverviewExample, DlVerticalOverviewExample, - DlSmallExample + DlSmallExample, + DlResizableExample ]; @NgModule({ diff --git a/packages/docs-examples/components/filter-bar/filter-bar-readonly-pipes/filter-bar-readonly-pipes-example.ts b/packages/docs-examples/components/filter-bar/filter-bar-readonly-pipes/filter-bar-readonly-pipes-example.ts index d8bc116a30..fd4a90a378 100644 --- a/packages/docs-examples/components/filter-bar/filter-bar-readonly-pipes/filter-bar-readonly-pipes-example.ts +++ b/packages/docs-examples/components/filter-bar/filter-bar-readonly-pipes/filter-bar-readonly-pipes-example.ts @@ -41,7 +41,7 @@ const createSearchPipe = (): KbqPipe => ({
- + Пользователь rturov diff --git a/packages/e2e/routes.ts b/packages/e2e/routes.ts index 9a4b5faadd..e86f6c522e 100644 --- a/packages/e2e/routes.ts +++ b/packages/e2e/routes.ts @@ -25,7 +25,7 @@ import { E2eCodeBlockStates } from '../components/code-block/e2e'; import { E2eContentPanelScrollOverflow, E2eContentPanelState } from '../components/content-panel/e2e'; import { E2eDatepickerPositioning, E2eDatepickerStates } from '../components/datepicker/e2e'; import { E2eDividerStateAndStyle } from '../components/divider/e2e'; -import { E2eDlStates } from '../components/dl/e2e'; +import { E2eDlResizable, E2eDlStates } from '../components/dl/e2e'; import { E2eDropdownNestedLtr, E2eDropdownNestedRtl, @@ -161,6 +161,7 @@ const components = [ E2eEmptyStateStateAndStyle, E2eCodeBlockStates, E2eDlStates, + E2eDlResizable, E2eAlertStateAndStyle, E2eDividerStateAndStyle, E2eButtonToggleStates, diff --git a/tools/public_api_guard/components/core.api.md b/tools/public_api_guard/components/core.api.md index 59ec202267..8728796ca7 100644 --- a/tools/public_api_guard/components/core.api.md +++ b/tools/public_api_guard/components/core.api.md @@ -451,6 +451,7 @@ export const enUSLocaleData: { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -716,6 +717,7 @@ export const esLALocaleData: { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -1223,6 +1225,7 @@ export function KBQ_DEFAULT_LOCALE_DATA_FACTORY(): { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -1463,6 +1466,7 @@ export function KBQ_DEFAULT_LOCALE_DATA_FACTORY(): { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -1698,6 +1702,7 @@ export function KBQ_DEFAULT_LOCALE_DATA_FACTORY(): { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -1938,6 +1943,7 @@ export function KBQ_DEFAULT_LOCALE_DATA_FACTORY(): { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -2175,6 +2181,7 @@ export function KBQ_DEFAULT_LOCALE_DATA_FACTORY(): { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -2469,6 +2476,7 @@ export type KbqA11yLocaleConfiguration = { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; // @public @@ -4455,6 +4463,7 @@ export const ptBRLocaleData: { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -4905,6 +4914,7 @@ export const ruRULocaleData: { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; @@ -5266,6 +5276,7 @@ export const tkTMLocaleData: { clear: string; showPassword: string; hidePassword: string; + resizeColumns: string; }; select: { hiddenItemsText: string; diff --git a/tools/public_api_guard/components/dl.api.md b/tools/public_api_guard/components/dl.api.md index 5c80ab79eb..8652a41ba8 100644 --- a/tools/public_api_guard/components/dl.api.md +++ b/tools/public_api_guard/components/dl.api.md @@ -4,62 +4,72 @@ ```ts -import { AfterContentInit } from '@angular/core'; -import * as i0 from '@angular/core'; +import * as _angular_core from '@angular/core'; import * as i1 from '@angular/cdk/a11y'; import * as i2 from '@angular/cdk/platform'; -import { OnDestroy } from '@angular/core'; -import { Subject } from 'rxjs'; +import { KbqResizerDirection } from '@koobiq/components/resizer'; +import { KbqResizerSizeChangeEvent } from '@koobiq/components/resizer'; // @public (undocumented) export class KbqDdComponent { // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: _angular_core.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // @public export type KbqDlAlign = 'start' | 'center' | 'end'; // @public (undocumented) -export class KbqDlComponent implements AfterContentInit, OnDestroy { - readonly horizontalAlign: i0.InputSignal; +export class KbqDlComponent { + constructor(); + readonly columnMinWidth: _angular_core.InputSignal; + readonly columnResizable: _angular_core.InputSignalWithTransform; + readonly columnWidth: _angular_core.ModelSignal; // (undocumented) - readonly minWidth: i0.InputSignal; + protected get currentColumnWidth(): number; + protected handleColumnResize(input: KbqResizerSizeChangeEvent): void; + protected handleResizeDblClick(event: MouseEvent): void; + protected handleResizeKeydown(event: KeyboardEvent): void; + readonly horizontalAlign: _angular_core.InputSignal; + protected readonly isVertical: _angular_core.Signal; // (undocumented) - ngAfterContentInit(): void; + protected get maxColumnWidth(): number; + // @deprecated + readonly minWidth: _angular_core.InputSignal; + protected readonly normalizedColumnMinWidth: _angular_core.Signal; + readonly remainingColumnMinWidth: _angular_core.InputSignal; + protected readonly resizeDirection: _angular_core.WritableSignal; + readonly resizerAriaLabel: _angular_core.InputSignal; + protected readonly resizerVisible: _angular_core.Signal; + protected readonly resolvedResizerAriaLabel: _angular_core.Signal; + readonly vertical: _angular_core.InputSignal; + readonly verticalAlign: _angular_core.InputSignal; + readonly verticalBreakpoint: _angular_core.InputSignalWithTransform; + readonly wide: _angular_core.InputSignal; // (undocumented) - ngOnDestroy(): void; + static ɵcmp: _angular_core.ɵɵComponentDeclaration; // (undocumented) - readonly resizeStream: Subject; - // (undocumented) - vertical: boolean | null; - readonly verticalAlign: i0.InputSignal; - // (undocumented) - readonly wide: i0.InputSignal; - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // @public (undocumented) export class KbqDlModule { // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; // (undocumented) - static ɵinj: i0.ɵɵInjectorDeclaration; + static ɵinj: _angular_core.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: _angular_core.ɵɵNgModuleDeclaration; } // @public (undocumented) export class KbqDtComponent { // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: _angular_core.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // (No @packageDocumentation comment for this package) diff --git a/tools/public_api_guard/components/resizer.api.md b/tools/public_api_guard/components/resizer.api.md index ef7a31e2a6..dcb1910736 100644 --- a/tools/public_api_guard/components/resizer.api.md +++ b/tools/public_api_guard/components/resizer.api.md @@ -18,11 +18,12 @@ export class KbqResizable { // @public export class KbqResizer { constructor(); - protected readonly cursor: i0.Signal<"default" | "ns-resize" | "ew-resize" | "nwse-resize" | "nesw-resize">; + readonly cursor: i0.InputSignal; readonly direction: i0.InputSignal; + protected readonly resolvedCursor: i0.Signal; readonly sizeChange: i0.OutputEmitterRef; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }