diff --git a/projects/demo/src/app/pages/table/table-demo.component.html b/projects/demo/src/app/pages/table/table-demo.component.html index e64fbc90..51c5b71e 100644 --- a/projects/demo/src/app/pages/table/table-demo.component.html +++ b/projects/demo/src/app/pages/table/table-demo.component.html @@ -20,9 +20,13 @@ [enableRowSelection]="_tableDemoService.model.enableRowSelection" [enableSorting]="_tableDemoService.model.enableSorting" [masterDetail]="_tableDemoService.model.masterDetail" + [masterDetailToggleColumnId]="_tableDemoService.model.masterDetailToggleColumnId" + [masterDetailTogglePosition]="_tableDemoService.model.masterDetailTogglePosition" + [masterDetailTriggerMode]="_tableDemoService.model.masterDetailTriggerMode" + [masterDetailTriggerTemplateName]="_tableDemoService.model.masterDetailTriggerTemplateName" [showDetailHeader]="_tableDemoService.model.showDetailHeader" [detailStickyColumns]="_tableDemoService.model.detailStickyColumns" - [hasDetailRow]="_hasDetailRow" + [hasDetailRow]="_hasDetailRow()" [isRowSelectable]="_isRowSelectable" [withBorder]="_tableDemoService.model.withBorder" (sortChange)="_dataSource.sort($event)" @@ -48,6 +52,16 @@ value | translate }} {{ row?.summary }} + +
+ {{ value }} + @if (detail.hasDetailRow) { + + } +
+
@@ -60,7 +74,7 @@ controls controlName="Table" configType="input" - [controlConfig]="_tableDemoService.inputControlConfig" + [controlConfig]="_tableDemoService.demoInputControlConfig" [(model)]="_tableDemoService.model" (resetted)="_tableDemoService.reset()" /> diff --git a/projects/demo/src/app/pages/table/table-demo.component.ts b/projects/demo/src/app/pages/table/table-demo.component.ts index a7d07cff..35d1b08f 100644 --- a/projects/demo/src/app/pages/table/table-demo.component.ts +++ b/projects/demo/src/app/pages/table/table-demo.component.ts @@ -6,7 +6,7 @@ import { DemoAndCodeComponent } from '../../components/tabs/demo-and-code/demo-a import { TryoutControlComponent } from '../../components/tryout/tryout-controls.component'; import { TryoutComponent } from '../../components/tryout/tryout.component'; -import { Component, DestroyRef, inject, Injectable, input, OnInit, viewChild, ViewEncapsulation } from '@angular/core'; +import { Component, computed, DestroyRef, inject, Injectable, input, OnInit, viewChild, ViewEncapsulation } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { IdsButtonComponent } from '@i-cell/ids-angular/button'; @@ -17,6 +17,7 @@ import { IdsTableCellTemplateDirective, IdsTableColumnDef, IdsTableComponent, + IdsTableDetailTriggerDirective, IdsTableIntl, IdsTableRequestFactory, IdsTableRowKeydownEvent, @@ -79,6 +80,7 @@ export class TableDemoTableIntl extends IdsTableIntl { IdsTableSortDescIconDirective, IdsTableSortNoneIconDirective, IdsTableComponent, + IdsTableDetailTriggerDirective, FormsModule, TranslateModule, TryoutComponent, @@ -107,6 +109,13 @@ export class TableDemoComponent implements OnInit { (paginationData, sortInfo) => this._tableDemoService.getPeriodicTable(paginationData, sortInfo); protected _dataSource = new ServerSideDataSource(this._requestFactory); + + protected _hasDetailRow = computed(() => { + const excludedSymbol = this._tableDemoService.excludedDetailSymbol(); + return (_index: number, row: PeriodicTableElement): boolean => + !!row.summary && (!excludedSymbol || row.symbol !== excludedSymbol); + }); + protected _totalRows = toSignal( this._dataSource.pageInfo$.pipe( map((pageInfo) => (pageInfo?.totalRows || 0)), @@ -154,10 +163,6 @@ export class TableDemoComponent implements OnInit { console.info('onContentChanged'); } - protected _hasDetailRow(index: number, rowData: PeriodicTableElement): boolean { - return !!rowData.summary; - } - protected _isRowSelectable(index: number, rowData: PeriodicTableElement): boolean { return !!rowData.number && rowData.number > 0; } diff --git a/projects/demo/src/app/pages/table/table-demo.service.ts b/projects/demo/src/app/pages/table/table-demo.service.ts index e21e204a..2330b73c 100644 --- a/projects/demo/src/app/pages/table/table-demo.service.ts +++ b/projects/demo/src/app/pages/table/table-demo.service.ts @@ -5,7 +5,7 @@ import { environment } from '../../../environments/environment'; import { WidgetDocsService } from '../../services/widget-docs.service'; import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable, signal } from '@angular/core'; import { DemoApiControlConfig } from '@demo-types/demo-api-control.type'; import { DemoControl, DemoControlConfig } from '@demo-types/demo-control.type'; import { DemoMethodConfig } from '@demo-types/demo-method.type'; @@ -19,6 +19,10 @@ import { IdsTableAppearance, IdsTableAppearanceType, IdsTableColumnDef, + IdsTableDetailTogglePosition, + IdsTableDetailTogglePositionType, + IdsTableDetailTriggerMode, + IdsTableDetailTriggerModeType, IdsTableRequestPaginationData, IdsTableResponseData, IdsTableSortDirection, @@ -44,6 +48,10 @@ export type TableInputControls = { clearSelectionOnChange: boolean; enableSorting: boolean; masterDetail: boolean; + masterDetailToggleColumnId: string | null; + masterDetailTogglePosition: IdsTableDetailTogglePositionType; + masterDetailTriggerMode: IdsTableDetailTriggerModeType; + masterDetailTriggerTemplateName: string; showDetailHeader: boolean; detailStickyColumns: boolean; withBorder: boolean; @@ -132,6 +140,7 @@ export class TableDemoService { }, { id: 'symbol', + field: 'symbol', label: 'TABLE.COL.SYMBOL', headerCellRenderer: 'translate2', cellRenderer: 'symbol', @@ -234,6 +243,42 @@ export class TableDemoService { default: defaultConfig.masterDetail, control: DemoControl.SWITCH, }, + masterDetailToggleColumnId: { + description: this._widgetDocs.getDescription(TABLE_DOCS_PATH, 'masterDetailToggleColumnId', + 'Visible data column ID for the master-detail toggle.'), + type: 'string | null', + default: defaultConfig.masterDetailToggleColumnId, + control: DemoControl.SELECT, + list: [ + '', + ...this.colDefs.filter((column) => column.visible !== false).map((column) => column.id), + ], + }, + masterDetailTogglePosition: { + description: this._widgetDocs.getDescription(TABLE_DOCS_PATH, 'masterDetailTogglePosition', 'Position of the default detail toggle.'), + type: 'IdsTableDetailTogglePositionType', + default: defaultConfig.masterDetailTogglePosition, + control: DemoControl.SELECT, + list: convertEnumToStringArray(IdsTableDetailTogglePosition), + }, + masterDetailTriggerMode: { + description: this._widgetDocs.getDescription(TABLE_DOCS_PATH, 'masterDetailTriggerMode', 'Default or custom detail trigger.'), + type: 'IdsTableDetailTriggerModeType', + default: defaultConfig.masterDetailTriggerMode, + control: DemoControl.SELECT, + list: convertEnumToStringArray(IdsTableDetailTriggerMode), + }, + masterDetailTriggerTemplateName: { + description: this._widgetDocs.getDescription(TABLE_DOCS_PATH, 'masterDetailTriggerTemplateName', + 'Custom detail trigger template name.'), + type: 'string', + default: defaultConfig.masterDetailTriggerTemplateName, + control: DemoControl.SELECT, + list: [ + defaultConfig.masterDetailTriggerTemplateName, + 'missingTemplate', + ], + }, showDetailHeader: { description: this._widgetDocs.getDescription( TABLE_DOCS_PATH, @@ -418,18 +463,41 @@ export class TableDemoService { }, ]; - public defaults = getDefaultFromDemoConfig(this.inputControlConfig); + public readonly excludedDetailSymbol = signal(''); - public model: TableInputControls = { ...this.defaults }; + public readonly demoInputControlConfig = { + ...this.inputControlConfig, + excludedDetailSymbol: { + description: 'Demo only: rows with this exact symbol have no detail row or trigger.', + type: 'string', + default: '', + control: DemoControl.TEXT, + onModelChange: (value?: string): void => this.excludedDetailSymbol.set(value ?? ''), + }, + }; + + public defaults = getDefaultFromDemoConfig(this.demoInputControlConfig); + + public model = { ...this.defaults }; public reset(): void { this.model = { ...this.defaults }; + this.excludedDetailSymbol.set(this.defaults.excludedDetailSymbol); } public getMethodConfig(): DemoMethodConfig[] { return [this.methodControlConfig]; } + public readonly detailTriggerPropControlConfig: DemoControlConfig = { + idsTableDetailTrigger: { + description: this._widgetDocs.getDescription('table/directives/detail-trigger.docs.json', 'idsTableDetailTrigger', + 'Binds the detail context to a native button, with IDS-owned state and accessibility.'), + type: 'IdsTableDetailTrigger', + default: '-', + }, + }; + public readonly cellTemplatePropControlConfig: DemoControlConfig = { templateName: { description: this._widgetDocs.getDescription( @@ -460,6 +528,10 @@ export class TableDemoService { title: getDemoApiTitle(this.translate, 'COMPONENTS.TABLE', 'API.PROPERTY_GROUP.CELL_TEMPLATE'), config: this.cellTemplatePropControlConfig, }, + { + title: getDemoApiTitle(this.translate, 'COMPONENTS.TABLE', 'API.PROPERTY_GROUP.DETAIL_TRIGGER'), + config: this.detailTriggerPropControlConfig, + }, ]; } diff --git a/projects/demo/src/assets/i18n/en.json b/projects/demo/src/assets/i18n/en.json index 723bf99b..757c615a 100644 --- a/projects/demo/src/assets/i18n/en.json +++ b/projects/demo/src/assets/i18n/en.json @@ -157,6 +157,10 @@ "LABEL": { "DETAIL_HEADER": "Actions" }, + "DETAIL_TRIGGERS": { + "OPEN": "Show details", + "CLOSE": "Hide details" + }, "BUTTON": { "EXPAND_DETAILS": "Expand details", "COLLAPSE_DETAILS": "Collapse details", @@ -1709,6 +1713,7 @@ "PROPERTY_GROUP": { "TITLE": "title properties", "CELL_TEMPLATE": "cell template directive properties", + "DETAIL_TRIGGER": "detail trigger directive properties", "ERROR_DEFINITION": "error definition directive properties", "PSEUDO_CHECKBOX": "pseudo-checkbox properties", "HEADER": "header properties", diff --git a/projects/demo/src/assets/i18n/hu.json b/projects/demo/src/assets/i18n/hu.json index 806c2b38..2360a0e3 100644 --- a/projects/demo/src/assets/i18n/hu.json +++ b/projects/demo/src/assets/i18n/hu.json @@ -157,6 +157,10 @@ "LABEL": { "DETAIL_HEADER": "Műveletek" }, + "DETAIL_TRIGGERS": { + "OPEN": "Részletek megjelenítése", + "CLOSE": "Részletek elrejtése" + }, "BUTTON": { "EXPAND_DETAILS": "Részletek kinyitása", "COLLAPSE_DETAILS": "Részletek becsukása", @@ -1705,6 +1709,7 @@ "PROPERTY_GROUP": { "TITLE": "címsor tulajdonságai", "CELL_TEMPLATE": "cellasablon direktíva tulajdonságai", + "DETAIL_TRIGGER": "részletező trigger direktíva tulajdonságai", "ERROR_DEFINITION": "hibadefiníció direktíva tulajdonságai", "PSEUDO_CHECKBOX": "pszeudo-checkbox tulajdonságai", "HEADER": "fejléc tulajdonságai", diff --git a/projects/widgets/table/components/cell-content/cell-content.component.html b/projects/widgets/table/components/cell-content/cell-content.component.html index eefd82f1..4fad6db6 100644 --- a/projects/widgets/table/components/cell-content/cell-content.component.html +++ b/projects/widgets/table/components/cell-content/cell-content.component.html @@ -1,34 +1,51 @@ - - -@if (_cellRendererComp()) { - -} @else { - @switch (_cellTemplateName()) { - @case ('strong') { - {{ _cellValue() }} - } - @case ('numeric') { -
{{ _numericCellValue() }}
- } - @case ('icon') { - - } - - @default { - @let template = _cellTemplate(); - @if (template) { - - - } @else { - -
{{ _cellValue() }}
- } - } - } -} - - + + +@if (detailTemplate(); as template) { + +} @else if (defaultDetailToggle()) { +
+ @if (detailTogglePosition() === 'before') { + + } +
+ +
+ @if (detailTogglePosition() === 'after') { + + } +
+} @else { + +} + + + + + @if (_cellRendererComp()) { + + } @else { + @switch (_cellTemplateName()) { + @case ('strong') { + {{ _cellValue() }} + } + @case ('numeric') { +
{{ _numericCellValue() }}
+ } + @case ('icon') { + + } + @default { + @let template = _cellTemplate(); + @if (template) { + + } @else { +
{{ _cellValue() }}
+ } + } + } + } +
diff --git a/projects/widgets/table/components/cell-content/cell-content.component.ts b/projects/widgets/table/components/cell-content/cell-content.component.ts index f26f0495..6acd542a 100644 --- a/projects/widgets/table/components/cell-content/cell-content.component.ts +++ b/projects/widgets/table/components/cell-content/cell-content.component.ts @@ -1,5 +1,7 @@ import { IdsTableCellRenderer } from '../../directives/cell-renderer'; import { IdsTableIntl } from '../../table-intl'; +import { IdsTableDetailTogglePosition, IdsTableDetailTogglePositionType } from '../../types/table-detail-toggle-position.type'; +import { IdsTableDetailTrigger } from '../../types/table-detail-trigger'; import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { Component, computed, ElementRef, inject, input, signal, TemplateRef } from '@angular/core'; @@ -28,6 +30,19 @@ export class IdsCellContentComponent extends IdsTableCellRenderer { private _numberFormat = toSignal(this._intl.changes.pipe(map(() => this._intl.numberFormat), startWith(this._intl.numberFormat))); public externalCellTemplates = input>>(); + public rowIndex = input(); + public detailTrigger = input(null); + public detailTemplate = input | null>(null); + public defaultDetailToggle = input | null>(null); + public detailTogglePosition = input(IdsTableDetailTogglePosition.BEFORE); + + protected _templateContext = computed(() => ({ + $implicit: this.rowData(), + index: this.rowIndex(), + colDef: this.colDef(), + cellValue: this._cellValue(), + detail: this.detailTrigger(), + })); private _cellRendererDef = computed(() => (this._isHeader ? this.colDef().headerCellRenderer : this.colDef().cellRenderer)); private _updateCellContent = signal(true); diff --git a/projects/widgets/table/directives/detail-trigger.docs.json b/projects/widgets/table/directives/detail-trigger.docs.json new file mode 100644 index 00000000..a4adf0e7 --- /dev/null +++ b/projects/widgets/table/directives/detail-trigger.docs.json @@ -0,0 +1,9 @@ +{ + "idsTableDetailTrigger": { + "description": { + "en": "Bind the custom cell context's detail object to a native button (including button[idsChip] and button[idsIconButton]). IDS owns toggling and aria-expanded/aria-disabled. Native Enter/Space activation is preserved without extra key handlers. Supply visible button text or an aria-label for icon-only controls. Click and key events bubble to the cell/row as with the default trigger. Non-button hosts are not supported.", + "hu": "Az egyedi cellakontextus detail objektumát natív gombhoz kell kötni (button[idsChip] és button[idsIconButton] is használható). A nyitást és az aria-expanded/aria-disabled értékét az IDS kezeli. Az Enter/Space natív működése külön billentyűkezelő nélkül megmarad. A gomb kapjon látható szöveget, csak ikon esetén aria-labelt. A kattintás és billentyűesemények az alap triggerhez hasonlóan továbbterjednek a cellára/sorra. Nem button elem nem támogatott." + }, + "type": "IdsTableDetailTrigger" + } +} diff --git a/projects/widgets/table/directives/detail-trigger.ts b/projects/widgets/table/directives/detail-trigger.ts new file mode 100644 index 00000000..34f45adb --- /dev/null +++ b/projects/widgets/table/directives/detail-trigger.ts @@ -0,0 +1,22 @@ +import { IdsTableDetailTrigger } from '../types/table-detail-trigger'; + +import { Directive, input } from '@angular/core'; + +@Directive({ + selector: 'button[idsTableDetailTrigger]', + host: { + 'type': 'button', + '[attr.aria-expanded]': 'detail().expanded', + '[attr.aria-disabled]': '!detail().hasDetailRow || null', + '(click)': '_toggle()', + }, +}) +export class IdsTableDetailTriggerDirective { + public detail = input.required({ alias: 'idsTableDetailTrigger' }); + + protected _toggle(): void { + if (this.detail().hasDetailRow) { + this.detail().toggle(); + } + } +} diff --git a/projects/widgets/table/public-api.ts b/projects/widgets/table/public-api.ts index 7b2b2e9f..4e980656 100644 --- a/projects/widgets/table/public-api.ts +++ b/projects/widgets/table/public-api.ts @@ -1,6 +1,7 @@ export * from './datasource/server-side-datasource'; export * from './directives/cell-renderer'; export * from './directives/cell-template'; +export * from './directives/detail-trigger'; export * from './directives/sort-icon-template'; export * from './types/pagination-params'; export * from './types/request-factory'; @@ -9,6 +10,9 @@ export * from './types/response-data'; export * from './types/table-appearance.type'; export * from './types/table-cell-click-event'; export * from './types/table-column-def'; +export * from './types/table-detail-toggle-position.type'; +export * from './types/table-detail-trigger'; +export * from './types/table-detail-trigger-mode.type'; export * from './types/table-row-click-event'; export * from './types/table-row-keydown-event'; export * from './types/table-sort-direction'; diff --git a/projects/widgets/table/table-defaults.ts b/projects/widgets/table/table-defaults.ts index cf6d9dc4..c90b770e 100644 --- a/projects/widgets/table/table-defaults.ts +++ b/projects/widgets/table/table-defaults.ts @@ -1,10 +1,13 @@ import { IdsTableAppearance, IdsTableAppearanceType } from './types/table-appearance.type'; +import { IdsTableDetailTogglePosition, IdsTableDetailTogglePositionType } from './types/table-detail-toggle-position.type'; +import { IdsTableDetailTriggerMode, IdsTableDetailTriggerModeType } from './types/table-detail-trigger-mode.type'; import { IdsTableVariant, IdsTableVariantType } from './types/table-variant.type'; import { InjectionToken } from '@angular/core'; import { IdsSize, IdsSizeType } from '@i-cell/ids-angular/core'; export const DEFAULT_MASTER_DETAIL_TEMPLATE_NAME = 'detail'; +export const DEFAULT_MASTER_DETAIL_TRIGGER_TEMPLATE_NAME = 'detailTrigger'; export interface IdsTableDefaultConfig { appearance?: IdsTableAppearanceType, @@ -13,6 +16,10 @@ export interface IdsTableDefaultConfig { fixedHeader?: boolean, enableSorting?: boolean, masterDetail?: boolean, + masterDetailToggleColumnId?: string | null, + masterDetailTogglePosition?: IdsTableDetailTogglePositionType, + masterDetailTriggerMode?: IdsTableDetailTriggerModeType, + masterDetailTriggerTemplateName?: string, detailTemplateName?: string, detailStickyColumns?: boolean, showDetailHeader?: boolean, @@ -38,6 +45,10 @@ export function IDS_TABLE_DEFAULT_CONFIG_FACTORY(): Required
+ + @if (detail.hasDetailRow) { + + } @else if (reserveSpace) { + + } + +
@@ -90,31 +107,23 @@ } - @let hasDetail = hasDetailRow()(idx, row); - @let rowExpanded = _expandedRows.has(row); - @if (hasDetail) { - + @let detail = _getDetailTrigger(row, idx); + @if (_customDetailTrigger()) { + + } @else { + } -
+
@let isDefaultTemplate = _detailTemplate() === _defaulDetailTemplate(); @let context = isDefaultTemplate ? { $implicit: row, cols: _hiddenColumns() } : { $implicit: row }; @@ -179,7 +188,7 @@ @if (col.identifier) { + > + } @else { + > + } } @@ -227,7 +248,7 @@ *cdkRowDef="let row; columns: _detailRowTemplates(); when: hasDetailRow()" cdk-row class="ids-table__detail-row ids-table__row--surface" - [class.ids-table__detail-row--expanded]="_expandedRows.has(row) || _allRowsExpanded()" + [class.ids-table__detail-row--expanded]="_expandedRows().has(row) || _allRowsExpanded()" > } diff --git a/projects/widgets/table/table.component.spec.ts b/projects/widgets/table/table.component.spec.ts index 3d296011..7d712b3b 100644 --- a/projects/widgets/table/table.component.spec.ts +++ b/projects/widgets/table/table.component.spec.ts @@ -1,23 +1,376 @@ -import { TableComponent } from './table.component'; +import { IdsTableCellTemplateDirective } from './directives/cell-template'; +import { IdsTableDetailTriggerDirective } from './directives/detail-trigger'; +import { IdsTableComponent } from './table.component'; +import { IdsTableColumnDef } from './types/table-column-def'; +import { IdsTableDetailTogglePositionType } from './types/table-detail-toggle-position.type'; +import { IdsTableDetailTriggerModeType } from './types/table-detail-trigger-mode.type'; +import { Component, viewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; -describe('TableComponent', () => { - let component: TableComponent; - let fixture: ComponentFixture; +interface TestRow { + name: string; + permissions: string; + detail: boolean; + showTrigger?: boolean; +} - beforeEach(async() => { - await TestBed.configureTestingModule({ - imports: [TableComponent], - }) - .compileComponents(); +@Component({ + imports: [ + IdsTableComponent, + IdsTableCellTemplateDirective, + IdsTableDetailTriggerDirective, + ], + template: ` + + Details for {{ row.name }} + {{ value }} + + @if (!emptyTemplate) { + {{ row.name }}:{{ index }}:{{ column?.id }}:{{ value }} + @if (row.showTrigger !== false) { + + } + @if (directToggle) { + + } + } + + + `, +}) +class TestHostComponent { + public table = viewChild.required(IdsTableComponent); + public target: string | null = null; + public masterDetail = true; + public position: IdsTableDetailTogglePositionType = 'before'; + public mode: IdsTableDetailTriggerModeType = 'default'; + public triggerTemplate = 'customTrigger'; + public emptyTemplate = false; + public directToggle = false; + public cellClicks = 0; + public rowClicks = 0; + public rowKeydowns = 0; + public detailStickyColumns = true; + public rowSelection = false; + public columns: IdsTableColumnDef[] = [ + { id: 'name', field: 'name', label: 'Name', identifier: true }, + { id: 'permissions', field: 'permissions', label: 'Permissions', stickyEnd: true, cellRenderer: 'permissions' }, + ]; - fixture = TestBed.createComponent(TableComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + public rows: TestRow[] = [ + { name: 'First', permissions: 'Read', detail: true }, + { name: 'Second', permissions: 'Write', detail: false }, + ]; - it('should create', () => { - expect(component).toBeTruthy(); - }); + public hasDetail = (index: number, row: TestRow): boolean => index === 0 && row.detail; +} + +let fixture: ComponentFixture; +let host: TestHostComponent; +let element: HTMLElement; + +beforeEach(async() => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent], + providers: [provideNoopAnimations()], + }).compileComponents(); + fixture = TestBed.createComponent(TestHostComponent); + host = fixture.componentInstance; + element = fixture.nativeElement as HTMLElement; +}); + +async function render(): Promise { + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); +} + +function toggleButton(): HTMLButtonElement { + return element.querySelector('.ids-table__button--master-detail')!; +} + +it('uses the trailing detail column when the new input is entirely omitted', async() => { + const tableFixture = TestBed.createComponent(IdsTableComponent); + tableFixture.componentRef.setInput('columnDefs', host.columns); + tableFixture.componentRef.setInput('dataSource', host.rows); + tableFixture.componentRef.setInput('masterDetail', true); + tableFixture.componentRef.setInput('hasDetailRow', host.hasDetail); + tableFixture.detectChanges(); + await tableFixture.whenStable(); + const tableElement = tableFixture.nativeElement as HTMLElement; + const lastCell = tableElement.querySelector('.ids-table__row')?.lastElementChild; + expect(lastCell?.classList.contains('ids-table__cell--master-detail-toggle')).toBe(true); + const button = tableElement.querySelector('.ids-table__button--master-detail')!; + button.click(); + tableFixture.detectChanges(); + await tableFixture.whenStable(); + expect(button.getAttribute('aria-expanded')).toBe('true'); + tableFixture.destroy(); +}); + +it('keeps the default toggle in the last sticky-end column and opens and closes the detail', async() => { + await render(); + const row = element.querySelector('.ids-table__row')!; + expect(row.lastElementChild?.classList.contains('ids-table__cell--master-detail-toggle')).toBe(true); + expect(row.lastElementChild?.classList.contains('cdk-table-sticky')).toBe(true); + expect(element.querySelectorAll('.ids-table__button--master-detail')).toHaveLength(1); + expect(toggleButton().getAttribute('aria-label')).toBe('Expand row details'); + expect(toggleButton().getAttribute('aria-expanded')).toBe('false'); + toggleButton().click(); + await render(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('true'); + expect(toggleButton().textContent).toContain('chevron-up'); + expect(element.querySelector('.ids-table__detail-row--expanded')?.textContent).toContain('Details for First'); + toggleButton().click(); + await render(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('false'); + expect(toggleButton().textContent).toContain('chevron-down'); + expect(element.querySelector('.ids-table__detail-row--expanded')).toBeNull(); +}); + +it.each([ + 'permissions', + 'name', +])('embeds in %s while retaining cell content and conditional toggles', async(target) => { + host.target = target; + await render(); + expect(element.querySelector('.ids-table__cell--master-detail-toggle')).toBeNull(); + expect(toggleButton().closest('td, th')?.classList.contains(`cdk-column-${target}`)).toBe(true); + expect(element.querySelector('.cdk-column-permissions strong')?.textContent).toBe('Read'); + expect(element.querySelectorAll('.ids-table__button--master-detail')).toHaveLength(1); + toggleButton().click(); + await render(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('true'); + expect(element.querySelector('.ids-table__detail-row--expanded')).not.toBeNull(); + host.table().collapseAll(); + await render(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('false'); + host.table().expandAll(); + await render(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('true'); +}); + +it.each([ + 'unknown', + 'permissions', +])('falls back for invalid or hidden target %s', async(target) => { + const warning = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + try { + host.target = target; + host.columns = host.columns.map((column) => ({ ...column, visible: column.id !== 'permissions' })); + await render(); + const lastCell = element.querySelector('.ids-table__row')?.lastElementChild; + expect(lastCell?.classList.contains('ids-table__cell--master-detail-toggle')).toBe(true); + expect(warning).toHaveBeenCalled(); + toggleButton().click(); + await render(); + expect(element.querySelector('.ids-table__detail-row--expanded')).not.toBeNull(); + } finally { + warning.mockRestore(); + } +}); + +it.each([ + 'sticky', + 'stickyEnd', +] as const)('keeps detail cells aligned for a %s target', async(stickySide) => { + host.target = 'permissions'; + host.rowSelection = true; + host.columns = host.columns.map((column) => ({ ...column, stickyEnd: false, [stickySide]: column.id === 'permissions' })); + await render(); + const detailRow = element.querySelector('.ids-table__detail-row')!; + const expectedCellCount = host.columns.length + 1; + expect(detailRow.children).toHaveLength(expectedCellCount); + expect(detailRow.querySelector('.ids-table__detail-cell')?.getAttribute('colspan')).toBe('1'); + host.detailStickyColumns = false; + await render(); + expect(element.querySelector('.ids-table__detail-row')?.children).toHaveLength(1); + expect(element.querySelector('.ids-table__detail-cell')?.getAttribute('colspan')).toBe('3'); +}); + +it('restores the trailing column when the target is cleared and handles replacement rows', async() => { + host.target = 'permissions'; + await render(); + host.target = null; + host.rows = [{ name: 'New page', permissions: 'Admin', detail: true }]; + await render(); + expect(element.querySelector('.ids-table__row')?.lastElementChild?.contains(toggleButton())).toBe(true); + toggleButton().click(); + await render(); + expect(element.querySelector('.ids-table__detail-row--expanded')?.textContent).toContain('New page'); + host.masterDetail = false; + await render(); + expect(element.querySelector('.ids-table__button--master-detail')).toBeNull(); +}); + +it('uses a full-width detail when all visible columns are sticky', async() => { + host.target = 'permissions'; + host.columns = host.columns.map((column) => ({ ...column, sticky: true, stickyEnd: false })); + await render(); + const detailRow = element.querySelector('.ids-table__detail-row')!; + expect(detailRow.children).toHaveLength(1); + expect(detailRow.querySelector('.ids-table__detail-cell')?.getAttribute('colspan')).toBe(String(host.columns.length)); +}); + +it.each([ + 'before', + 'after', +] as const)('renders the default toggle %s the original content', async(position) => { + host.target = 'permissions'; + host.position = position; + await render(); + const wrapper = toggleButton().parentElement!; + expect(wrapper.classList.contains('ids-table__cell-content--with-detail-toggle')).toBe(true); + expect(position === 'before' ? wrapper.firstElementChild : wrapper.lastElementChild).toBe(toggleButton()); + expect(wrapper.querySelector('strong')?.textContent).toBe('Read'); +}); + +it.each([ + null, + 'name', + 'permissions', +])('renders a custom full cell in %s and synchronizes state', async(target) => { + host.target = target; + host.mode = 'custom'; + await render(); + const button = element.querySelector('.custom-trigger')!; + expect(toggleButton()).toBeNull(); + expect(element.querySelector('.custom-content')?.textContent).toContain(`First:0:${target ?? ''}:`); + expect(button.getAttribute('aria-expanded')).toBe('false'); + button.click(); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('true'); + expect(button.textContent).toBe('Close'); + expect(host.rowClicks).toBe(1); + expect(host.cellClicks).toBe(target ? 1 : 0); + host.table().collapseAll(); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('false'); + host.table().expandAll(); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('true'); +}); + +it('does not open a row rejected by hasDetailRow even with a custom trigger', async() => { + host.target = 'name'; + host.mode = 'custom'; + await render(); + const button = element.querySelectorAll('.custom-trigger')[1]; + expect(button.getAttribute('aria-disabled')).toBe('true'); + button.click(); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('false'); + expect(element.querySelectorAll('.ids-table__detail-row--expanded')).toHaveLength(0); + expect(button.closest('th')?.textContent).toContain('Second'); + host.table().expandAll(); + await render(); + expect(element.querySelectorAll('.ids-table__detail-row--expanded')).toHaveLength(1); + expect(button.getAttribute('aria-expanded')).toBe('false'); +}); + +it('guards the exported toggle callback as well as the directive', async() => { + host.mode = 'custom'; + host.directToggle = true; + await render(); + element.querySelectorAll('.direct-toggle')[1].click(); + await render(); + expect(element.querySelector('.ids-table__detail-row--expanded')).toBeNull(); +}); + +it('does not replace intentionally omitted custom triggers or empty templates', async() => { + const warning = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + try { + host.mode = 'custom'; + host.rows = [{ ...host.rows[0], showTrigger: false }]; + await render(); + expect(element.querySelector('button')).toBeNull(); + host.table().expandAll(); + await render(); + expect(element.querySelector('.ids-table__detail-row--expanded')).not.toBeNull(); + host.emptyTemplate = true; + await render(); + expect(element.querySelector('.ids-table__cell--master-detail-toggle')?.textContent?.trim()).toBe(''); + expect(warning).not.toHaveBeenCalled(); + } finally { + warning.mockRestore(); + } +}); + +it.each([ + null, + 'permissions', +])('warns for missing custom templates in %s without a chevron fallback', async(target) => { + const warning = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + try { + host.mode = 'custom'; + host.target = target; + host.triggerTemplate = 'missing'; + await render(); + expect(toggleButton()).toBeNull(); + expect(element.querySelector('.cdk-column-permissions strong')?.textContent).toBe('Read'); + expect(element.querySelector('.ids-table__cell--master-detail-toggle')?.textContent?.trim() ?? '').toBe(''); + expect(warning).toHaveBeenCalledWith(expect.stringContaining('"missing"')); + } finally { + warning.mockRestore(); + } +}); + +it('leaves native keyboard activation to the button without swallowing row keydown', async() => { + host.mode = 'custom'; + await render(); + const button = element.querySelector('.custom-trigger')!; + expect(button.getAttribute('type')).toBe('button'); + button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('false'); + expect(host.rowKeydowns).toBe(1); + button.click(); + await render(); + expect(button.getAttribute('aria-expanded')).toBe('true'); +}); + +it('updates custom contexts after page replacement, sorting and mode changes', async() => { + host.mode = 'custom'; + host.target = 'name'; + host.hasDetail = (_index, row): boolean => row.detail; + await render(); + host.rows = [...host.rows].reverse(); + await render(); + expect(element.querySelector('.custom-content')?.textContent).toBe('Second:0:name:Second'); + host.rows = [{ name: 'Next page', permissions: 'Admin', detail: true }]; + await render(); + element.querySelector('.custom-trigger')!.click(); + await render(); + expect(element.querySelector('.ids-table__detail-row--expanded')?.textContent).toContain('Next page'); + host.mode = 'default'; + await render(); + expect(element.querySelector('.custom-trigger')).toBeNull(); + expect(toggleButton().getAttribute('aria-expanded')).toBe('true'); +}); + +it.each([ + 'sticky', + 'stickyEnd', +] as const)('keeps custom %s cells aligned with either detail span setting', async(stickySide) => { + host.mode = 'custom'; + host.target = 'permissions'; + host.columns = host.columns.map((column) => ({ ...column, stickyEnd: false, [stickySide]: column.id === 'permissions' })); + await render(); + const cell = element.querySelector('.custom-trigger')!.closest('td')!; + expect(cell.classList.contains('cdk-table-sticky')).toBe(true); + expect(element.querySelector('.ids-table__row')?.children).toHaveLength(host.columns.length); + expect(element.querySelector('.ids-table__detail-row')?.children).toHaveLength(host.columns.length); + host.detailStickyColumns = false; + await render(); + expect(element.querySelector('.ids-table__detail-row')?.children).toHaveLength(1); + expect(element.querySelector('.ids-table__detail-cell')?.getAttribute('colspan')).toBe(String(host.columns.length)); }); diff --git a/projects/widgets/table/table.component.ts b/projects/widgets/table/table.component.ts index ed9f75ed..fe7f6bde 100644 --- a/projects/widgets/table/table.component.ts +++ b/projects/widgets/table/table.component.ts @@ -1,5 +1,6 @@ import { IdsCellContentComponent } from './components/cell-content/cell-content.component'; import { IdsTableCellTemplateDirective } from './directives/cell-template'; +import { IdsTableDetailTriggerDirective } from './directives/detail-trigger'; import { RowInfoHolderDirective } from './directives/row-info-holder'; import { IdsTableSortAscIconDirective, IdsTableSortDescIconDirective, IdsTableSortNoneIconDirective } from './directives/sort-icon-template'; import { DEFAULT_MASTER_DETAIL_TEMPLATE_NAME, IDS_TABLE_DEFAULT_CONFIG, IDS_TABLE_DEFAULT_CONFIG_FACTORY, IdsTableDefaultConfig } from './table-defaults'; @@ -7,6 +8,9 @@ import { IdsTableIntl } from './table-intl'; import { IdsTableAppearance, IdsTableAppearanceType } from './types/table-appearance.type'; import { IdsTableCellClickEvent } from './types/table-cell-click-event'; import { IdsTableColumnDef } from './types/table-column-def'; +import { IdsTableDetailTogglePositionType } from './types/table-detail-toggle-position.type'; +import { IdsTableDetailTrigger } from './types/table-detail-trigger'; +import { IdsTableDetailTriggerMode, IdsTableDetailTriggerModeType } from './types/table-detail-trigger-mode.type'; import { IdsTableRowClickEvent } from './types/table-row-click-event'; import { IdsTableRowKeydownEvent } from './types/table-row-keydown-event'; import { IdsTableSortDirection, IdsTableSortDirectionType } from './types/table-sort-direction'; @@ -17,12 +21,12 @@ import { animate, state, style, transition, trigger } from '@angular/animations' import { SelectionModel } from '@angular/cdk/collections'; import { CdkCell, CdkCellDef, CdkColumnDef, CdkHeaderCell, CdkHeaderCellDef, CdkHeaderRow, CdkHeaderRowDef, CdkNoDataRow, CdkRow, CdkRowDef, CdkTable, CdkTableDataSourceInput } from '@angular/cdk/table'; import { NgClass, NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, contentChild, contentChildren, inject, input, OnInit, output, signal, TemplateRef, viewChildren, ViewEncapsulation } from '@angular/core'; +import { afterRenderEffect, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, contentChild, contentChildren, effect, inject, input, isDevMode, OnInit, output, signal, TemplateRef, untracked, viewChild, viewChildren, ViewEncapsulation } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { IdsCheckboxComponent } from '@i-cell/ids-angular/checkbox'; import { coerceBooleanAttribute, ComponentBaseWithDefaults, IdsSizeType } from '@i-cell/ids-angular/core'; import { IdsIconComponent } from '@i-cell/ids-angular/icon'; -import { IDS_ICON_BUTTON_PARENT, IdsIconButtonAppearance, IdsIconButtonAppearanceType, IdsIconButtonComponent, IdsIconButtonParent, IdsIconButtonVariant, IdsIconButtonVariantType } from '@i-cell/ids-angular/icon-button'; +import { IDS_ICON_BUTTON_DEFAULT_CONFIG, IDS_ICON_BUTTON_DEFAULT_CONFIG_FACTORY, IDS_ICON_BUTTON_PARENT, IdsIconButtonAppearance, IdsIconButtonAppearanceType, IdsIconButtonComponent, IdsIconButtonParent, IdsIconButtonVariant, IdsIconButtonVariantType } from '@i-cell/ids-angular/icon-button'; const defaultConfig = IDS_TABLE_DEFAULT_CONFIG_FACTORY(); @@ -34,6 +38,7 @@ const defaultConfig = IDS_TABLE_DEFAULT_CONFIG_FACTORY(); IdsIconComponent, IdsIconButtonComponent, IdsTableCellTemplateDirective, + IdsTableDetailTriggerDirective, CdkCell, CdkCellDef, CdkColumnDef, @@ -74,11 +79,14 @@ export class IdsTableComponent protected readonly _appearanceZebra = IdsTableAppearance.ZEBRA; protected readonly _defaultMasterDetailTemplateName = DEFAULT_MASTER_DETAIL_TEMPLATE_NAME; protected readonly _defaultConfig = this._getDefaultConfig(defaultConfig, IDS_TABLE_DEFAULT_CONFIG); + protected readonly _detailToggleSize = inject(IDS_ICON_BUTTON_DEFAULT_CONFIG).size ?? IDS_ICON_BUTTON_DEFAULT_CONFIG_FACTORY().size; private _cdRef = inject(ChangeDetectorRef); protected _intl = inject(IdsTableIntl); private _viewCellTemplates = viewChildren(IdsTableCellTemplateDirective); + private _cdkTable = viewChild(CdkTable); + private _refreshingDetailRows = false; private _rowDataHolders = viewChildren(RowInfoHolderDirective); private _selectorCheckboxes = viewChildren(IdsCheckboxComponent); private _cellContentRenderers = viewChildren(IdsCellContentComponent); @@ -96,6 +104,10 @@ export class IdsTableComponent public enableSorting = input(this._defaultConfig.enableSorting, { transform: coerceBooleanAttribute }); /** Enable master-detail rows */ public masterDetail = input(this._defaultConfig.masterDetail, { transform: coerceBooleanAttribute }); + public masterDetailToggleColumnId = input(this._defaultConfig.masterDetailToggleColumnId); + public masterDetailTogglePosition = input(this._defaultConfig.masterDetailTogglePosition); + public masterDetailTriggerMode = input(this._defaultConfig.masterDetailTriggerMode); + public masterDetailTriggerTemplateName = input(this._defaultConfig.masterDetailTriggerTemplateName); /** Name of the detail row renderer template */ public detailTemplateName = input(this._defaultConfig.detailTemplateName); /** If true the detail cell will not span sticky and stickyEnd columns. If false the detail cell will span the table's full width */ @@ -178,7 +190,7 @@ export class IdsTableComponent /** The row selection model for the table */ public rowSelection: SelectionModel = new SelectionModel(true, [], true); - protected _expandedRows = new Set(); + protected _expandedRows = signal>(new Set()); protected _allRowsExpanded = signal(false); protected _sortState = signal(null); @@ -194,6 +206,21 @@ export class IdsTableComponent (this.withBorder() ? 'with-border' : null), ])); + protected _masterDetailToggleColumn = computed(() => { + const columnId = this.masterDetailToggleColumnId(); + return this.masterDetail() && columnId ? + this.columnDefs().find((column) => column.id === columnId && column.visible !== false) ?? null : null; + }); + + protected _hasSeparateMasterDetailColumn = computed(() => this.masterDetail() && !this._masterDetailToggleColumn()); + + private _validateMasterDetailToggleColumn = effect(() => { + const columnId = this.masterDetailToggleColumnId(); + if (isDevMode() && this.masterDetail() && columnId && !this._masterDetailToggleColumn()) { + console.warn(`ids-table: masterDetailToggleColumnId "${columnId}" is not a visible column. Using the separate master-detail column.`); + } + }); + protected _actualColumns = computed(() => { const cols = this.columnDefs() // Filter non-visible columns @@ -217,7 +244,7 @@ export class IdsTableComponent }, [[], [], []]) .flat(); - if (this.masterDetail()) { + if (this._hasSeparateMasterDetailColumn()) { cols.push('$masterDetail'); } @@ -234,25 +261,33 @@ export class IdsTableComponent .map((colDef) => ({ ...colDef, visible: true })), ); + private _detailSpansStickyColumns = computed(() => !this.detailStickyColumns() || + (!!this._masterDetailToggleColumn() && !this.columnDefs().some((column) => + column.visible !== false && !column.sticky && !column.stickyEnd)), + ); + protected _detailRowTemplates = computed(() => { - if (!this.detailStickyColumns()) { + if (this._detailSpansStickyColumns()) { return ['$expandedDetail']; } + const columnDefs = this._masterDetailToggleColumn() ? + this.columnDefs().filter((column) => column.visible !== false) : this.columnDefs(); + return [ ...(this.enableRowSelection() ? ['$empty'] : []), - ...this.columnDefs().filter((colDef) => colDef.sticky).map(() => '$empty'), + ...columnDefs.filter((colDef) => colDef.sticky).map(() => '$empty'), '$expandedDetail', - ...this.columnDefs().filter((colDef) => colDef.stickyEnd).map(() => '$emptyEnd'), - ...(this.masterDetail() ? ['$emptyEnd'] : []), + ...columnDefs.filter((colDef) => colDef.stickyEnd).map(() => '$emptyEnd'), + ...(this._hasSeparateMasterDetailColumn() ? ['$emptyEnd'] : []), ]; }); protected _detailColSpan = computed(() => { const colDefs = this.columnDefs(); - let detailColSpan = colDefs.length; + let detailColSpan = this._masterDetailToggleColumn() ? this._actualColumns().length : colDefs.length; - if (this.detailStickyColumns()) { + if (!this._detailSpansStickyColumns()) { detailColSpan = colDefs.filter((def) => (!def.sticky && !def.stickyEnd && def.visible !== false)).length; } @@ -278,6 +313,49 @@ export class IdsTableComponent protected _detailTemplate = computed(() => this._cellTemplatesByName().get(this.detailTemplateName())); + protected _customDetailTrigger = computed(() => this.masterDetailTriggerMode() === IdsTableDetailTriggerMode.CUSTOM); + + protected _detailTriggerTemplate = computed(() => (this._customDetailTrigger() ? + this._cellTemplatesByName().get(this.masterDetailTriggerTemplateName()) ?? null : null), + ); + + private _validateDetailTriggerTemplate = afterRenderEffect(() => { + if (isDevMode() && this.masterDetail() && this._customDetailTrigger() && !this._detailTriggerTemplate()) { + console.warn(`ids-table: custom master-detail trigger template "${this.masterDetailTriggerTemplateName()}" was not found. ` + + 'No default trigger will be rendered.'); + } + }); + + private _refreshDetailRows = afterRenderEffect(() => { + const hasDetailRow = this.hasDetailRow(); + const masterDetail = this.masterDetail(); + const table = this._cdkTable(); + + if (!masterDetail || !table) { + return; + } + + untracked(() => { + const expandedRows = new Set(this._expandedRows()); + this.rowData().forEach((row, index) => { + if (!hasDetailRow(index, row)) { + expandedRows.delete(row); + } + }); + if (expandedRows.size !== this._expandedRows().size) { + this._expandedRows.set(expandedRows); + } + + this._refreshingDetailRows = true; + try { + table.renderRows(); + } finally { + this._refreshingDetailRows = false; + } + this._cdRef.markForCheck(); + }); + }); + protected _hasNoRows = computed(() => this.rowData().length === 0); protected _masterDetailEnabled = computed(() => @@ -329,11 +407,13 @@ export class IdsTableComponent * Open up all row details. */ public expandAll(): void { + const expandedRows = new Set(this._expandedRows()); this.rowData().forEach((rowData, idx) => { - if (!this._expandedRows.has(rowData) && this.hasDetailRow()(idx, rowData)) { - this._expandedRows.add(rowData); + if (!expandedRows.has(rowData) && this.hasDetailRow()(idx, rowData)) { + expandedRows.add(rowData); } }); + this._expandedRows.set(expandedRows); this._cdRef.markForCheck(); } @@ -341,7 +421,7 @@ export class IdsTableComponent * Closes all opened row details. */ public collapseAll(): void { - this._expandedRows.clear(); + this._expandedRows.set(new Set()); this._cdRef.markForCheck(); } @@ -355,7 +435,7 @@ export class IdsTableComponent protected _tableContentChanged(): void { this.contentChanged.emit(); - if (this.clearSelectionOnChange()) { + if (!this._refreshingDetailRows && this.clearSelectionOnChange()) { this.rowSelection.clear(); } } @@ -388,12 +468,28 @@ export class IdsTableComponent this.rowKeydown.emit({ event, rowData }); } - protected _handleMasterDetailClick(rowData: D): void { - if (this._expandedRows.has(rowData)) { - this._expandedRows.delete(rowData); + protected _getDetailTrigger(rowData: D, index: number): IdsTableDetailTrigger { + const hasDetailRow = this.masterDetail() && this.hasDetailRow()(index, rowData); + return { + expanded: hasDetailRow && this._expandedRows().has(rowData), + hasDetailRow, + ariaLabel: this._intl.getDetailExpandButtonAriaLabel(rowData), + toggle: () => this._handleMasterDetailClick(rowData), + }; + } + + private _handleMasterDetailClick(rowData: D): void { + const index = this.rowData().indexOf(rowData); + if (!this.masterDetail() || index < 0 || !this.hasDetailRow()(index, rowData)) { + return; + } + const expandedRows = new Set(this._expandedRows()); + if (expandedRows.has(rowData)) { + expandedRows.delete(rowData); } else { - this._expandedRows.add(rowData); + expandedRows.add(rowData); } + this._expandedRows.set(expandedRows); this._cdRef.markForCheck(); } diff --git a/projects/widgets/table/types/table-detail-toggle-position.type.ts b/projects/widgets/table/types/table-detail-toggle-position.type.ts new file mode 100644 index 00000000..1bdc06a1 --- /dev/null +++ b/projects/widgets/table/types/table-detail-toggle-position.type.ts @@ -0,0 +1,6 @@ +export const IdsTableDetailTogglePosition = { + BEFORE: 'before', + AFTER: 'after', +} as const; + +export type IdsTableDetailTogglePositionType = (typeof IdsTableDetailTogglePosition)[keyof typeof IdsTableDetailTogglePosition]; diff --git a/projects/widgets/table/types/table-detail-trigger-mode.type.ts b/projects/widgets/table/types/table-detail-trigger-mode.type.ts new file mode 100644 index 00000000..d6584665 --- /dev/null +++ b/projects/widgets/table/types/table-detail-trigger-mode.type.ts @@ -0,0 +1,6 @@ +export const IdsTableDetailTriggerMode = { + DEFAULT: 'default', + CUSTOM: 'custom', +} as const; + +export type IdsTableDetailTriggerModeType = (typeof IdsTableDetailTriggerMode)[keyof typeof IdsTableDetailTriggerMode]; diff --git a/projects/widgets/table/types/table-detail-trigger.ts b/projects/widgets/table/types/table-detail-trigger.ts new file mode 100644 index 00000000..f45f4268 --- /dev/null +++ b/projects/widgets/table/types/table-detail-trigger.ts @@ -0,0 +1,16 @@ +import { IdsTableColumnDef } from './table-column-def'; + +export interface IdsTableDetailTrigger { + readonly expanded: boolean; + readonly hasDetailRow: boolean; + readonly ariaLabel: string; + readonly toggle: () => void; +} + +export interface IdsTableDetailCellContext { + $implicit: D; + index: number; + colDef: IdsTableColumnDef | null; + cellValue: unknown; + detail: IdsTableDetailTrigger; +}