From e35eaad90656d51df8078e8906cd48ceec879adf Mon Sep 17 00:00:00 2001 From: tianwenjie Date: Thu, 8 Dec 2022 09:47:32 +0800 Subject: [PATCH] feat: table supports the use of attributes --- src/table/index.ts | 5 + src/table/new/table-cell.directive.ts | 27 +++ src/table/new/table-header-cell.directive.ts | 23 ++ src/table/new/table-header-row.component.ts | 22 ++ src/table/new/table-row.component.ts | 44 ++++ src/table/new/table-scroll.scss | 230 +++++++++++++++++++ src/table/new/table.component.scss | 212 +++++++++++++++++ src/table/new/table.component.ts | 102 ++++++++ src/table/table.module.ts | 15 ++ stories/table/table.stories.mdx | 58 +++-- 10 files changed, 720 insertions(+), 18 deletions(-) create mode 100644 src/table/new/table-cell.directive.ts create mode 100644 src/table/new/table-header-cell.directive.ts create mode 100644 src/table/new/table-header-row.component.ts create mode 100644 src/table/new/table-row.component.ts create mode 100644 src/table/new/table-scroll.scss create mode 100644 src/table/new/table.component.scss create mode 100644 src/table/new/table.component.ts diff --git a/src/table/index.ts b/src/table/index.ts index 0507009d6..8bd1c0ef2 100644 --- a/src/table/index.ts +++ b/src/table/index.ts @@ -1,3 +1,8 @@ +export * from './new/table.component'; +export * from './new/table-cell.directive'; +export * from './new/table-header-cell.directive'; +export * from './new/table-header-row.component'; +export * from './new/table-row.component'; export * from './table.component'; export * from './table.module'; export * from './table-cell.component'; diff --git a/src/table/new/table-cell.directive.ts b/src/table/new/table-cell.directive.ts new file mode 100644 index 000000000..a6e76158b --- /dev/null +++ b/src/table/new/table-cell.directive.ts @@ -0,0 +1,27 @@ +import { CdkCell, CdkColumnDef } from '@angular/cdk/table'; +import { Directive, ElementRef, Input } from '@angular/core'; + +import { buildBem } from '../../utils'; + +const bem = buildBem('aui-table'); + +/** Cell template container that adds the right classes and role. */ +@Directive({ + selector: '[auiTableCell]', + host: { + class: 'aui-table__cell', + role: 'gridcell', + '[class.aui-table__cell--column]': 'direction === "column"', + }, +}) +export class NewTableCellDirective extends CdkCell { + @Input() + direction: 'row' | 'column' = 'row'; + + constructor(columnDef: CdkColumnDef, elementRef: ElementRef) { + super(columnDef, elementRef); + elementRef.nativeElement.classList.add( + bem.element(`column-${columnDef.cssClassFriendlyName}`), + ); + } +} diff --git a/src/table/new/table-header-cell.directive.ts b/src/table/new/table-header-cell.directive.ts new file mode 100644 index 000000000..bc89f3796 --- /dev/null +++ b/src/table/new/table-header-cell.directive.ts @@ -0,0 +1,23 @@ +import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table'; +import { Directive, ElementRef } from '@angular/core'; + +import { buildBem } from '../../utils'; + +const bem = buildBem('aui-table'); + +/** Header cell template container that adds the right classes and role. */ +@Directive({ + selector: '[auiTableHeaderCell]', + host: { + class: 'aui-table__header-cell', + role: 'columnheader', + }, +}) +export class NewTableHeaderCellDirective extends CdkHeaderCell { + constructor(columnDef: CdkColumnDef, elementRef: ElementRef) { + super(columnDef, elementRef); + elementRef.nativeElement.classList.add( + bem.element(`column-${columnDef.cssClassFriendlyName}`), + ); + } +} diff --git a/src/table/new/table-header-row.component.ts b/src/table/new/table-header-row.component.ts new file mode 100644 index 000000000..2d862abed --- /dev/null +++ b/src/table/new/table-header-row.component.ts @@ -0,0 +1,22 @@ +import { CDK_ROW_TEMPLATE, CdkHeaderRow } from '@angular/cdk/table'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; + +/** Header template container that contains the cell outlet. Adds the right class and role. */ +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'tr[auiTableHeaderRow]', + template: CDK_ROW_TEMPLATE, + host: { + class: 'aui-table__header-row', + role: 'row', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'auiTableHeaderRow', + preserveWhitespaces: false, +}) +export class NewTableHeaderRowComponent extends CdkHeaderRow {} diff --git a/src/table/new/table-row.component.ts b/src/table/new/table-row.component.ts new file mode 100644 index 000000000..0c2f8c0be --- /dev/null +++ b/src/table/new/table-row.component.ts @@ -0,0 +1,44 @@ +import { CDK_ROW_TEMPLATE, CdkRow } from '@angular/cdk/table'; +import { + AfterContentInit, + ChangeDetectionStrategy, + Component, + ElementRef, + HostBinding, + Input, + ViewEncapsulation, +} from '@angular/core'; + +/** Data row template container that contains the cell outlet. Adds the right class and role. */ +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'tr[auiTableRow]', + template: CDK_ROW_TEMPLATE, + host: { + class: 'aui-table__row', + role: 'row', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'auiTableRow', + preserveWhitespaces: false, +}) +export class NewTableRowComponent extends CdkRow implements AfterContentInit { + @Input() + @HostBinding('class.isDisabled') + disabled = false; + + @HostBinding('class.hasPanel') + hasPanel = false; + + constructor(private readonly elRef: ElementRef) { + super(); + } + + ngAfterContentInit() { + const panel = this.elRef.nativeElement.querySelector( + '[auiTableCell][auiExpandPanel]', + ); + this.hasPanel = !!panel; + } +} diff --git a/src/table/new/table-scroll.scss b/src/table/new/table-scroll.scss new file mode 100644 index 000000000..461c57608 --- /dev/null +++ b/src/table/new/table-scroll.scss @@ -0,0 +1,230 @@ +@import '../../theme/var'; +@import '../../theme/mixin'; + +$stickyCssClass: 'aui-table-sticky'; + +// stylelint-disable plugin/no-low-performance-animation-properties + +// style for column shadow +.aui-table__scroll-wrapper { + display: flex; + flex-direction: column; + max-height: 100%; + overflow: hidden; + background-color: use-rgb(n-9); + padding: 0 $table-padding $table-padding; + border-radius: use-var(border-radius-l); + @include scroll-bar; + + .aui-table { + padding: 0; + border-radius: 0; + } + + .aui-table__scroll-shadow { + &.hasTableTopShadow:before, + &.hasTableBottomShadow:after { + transform: none; + width: 100%; + left: 0; + } + } +} + +// style for vertical shadow +.aui-table__scroll-shadow { + &.aui-table { + overflow: auto; + @include scroll-bar; + } + + &.hasTableTopShadow:before { + content: ''; + position: sticky; + display: block; + height: 16px; + margin: -16px -12px 0; + z-index: 99; + top: 28px; + @include theme-light { + box-shadow: 0 10px 10px -4px use-rgba(n-1, 0.16); + } + @include theme-dark { + box-shadow: 0 10px 10px -4px use-rgba(n-9, 0.75); + } + } + + &.hasTableBottomShadow:after { + content: ''; + position: sticky; + display: block; + height: 16px; + transform: translate3d(0, 12px, 0); + z-index: 99; + bottom: 0; + margin: -16px -12px 0; + @include theme-light { + box-shadow: 0 -10px 10px -4px use-rgba(n-1, 0.16) inset; + } + @include theme-dark { + box-shadow: 0 -10px 10px -4px use-rgba(n-9, 0.75) inset; + } + } + + .aui-table__header-row { + margin: 0; + padding: 0; + align-items: stretch; + + .aui-table__header-cell { + &:first-of-type { + padding-left: $table-cell-padding-h * 2; + } + + &:last-of-type { + padding-right: $table-cell-padding-h * 2; + } + } + + + .aui-table__row { + .aui-table__cell { + &:first-of-type { + border-top-left-radius: use-var(border-radius-l); + } + + &:last-of-type { + border-top-right-radius: use-var(border-radius-l); + } + } + } + } + + .aui-table__row { + border: none; + padding: 0; + align-items: stretch; + min-height: $table-row-min-height + 1; + + .aui-table__cell { + border-width: 1px 0; + border-style: solid; + border-color: use-rgb(n-8); + + &:first-of-type { + border-left-width: 1px; + padding-left: $table-cell-padding-h * 2 - 1; + } + + &:last-of-type { + border-right-width: 1px; + padding-right: $table-cell-padding-h * 2 - 1; + } + } + + &:last-child { + min-height: $table-row-min-height + 2; + + .aui-table__cell { + &:first-of-type { + border-bottom-left-radius: use-var(border-radius-l); + } + + &:last-of-type { + border-bottom-right-radius: use-var(border-radius-l); + } + } + } + + &:not(&:last-child) { + .aui-table__cell { + border-bottom-width: 0; + } + } + } + + &--has-scroll { + .#{$stickyCssClass} { + &-border-elem-left:after, + &-border-elem-right:after { + position: absolute; + top: 0; + bottom: -1px; + width: 20px; + transition: box-shadow 0.3s; + content: ''; + pointer-events: none; + } + + &-border-elem-left:before, + &-border-elem-right:before { + position: absolute; + top: 0; + bottom: -1px; + content: ''; + @include vertical-dashed-line(1px, 'n-7'); + } + + &-border-elem-left { + padding-right: $table-cell-padding-h * 3; + + &:after { + right: -$table-cell-padding-h; + } + + &:before { + right: $table-cell-padding-h; + } + } + + &-border-elem-right { + padding-left: $table-cell-padding-h * 3; + + &:after { + left: -$table-cell-padding-h; + } + + &:before { + left: $table-cell-padding-h; + } + } + } + } + + &--scrolling { + .#{$stickyCssClass} { + &-border-elem-left:not(.aui-table__header-row) { + &:after { + @include theme-light { + box-shadow: inset 8px 0 4px -4px use-rgba(n-1, 0.16); + } + @include theme-dark { + box-shadow: inset 8px 0 4px -4px use-rgba(n-9, 0.75); + } + } + + &:before { + @include vertical-dashed-line(1px, 'primary'); + } + } + } + } + + &--before-end { + .#{$stickyCssClass} { + &-border-elem-right:not(.aui-table__header-row) { + &:after { + @include theme-light { + box-shadow: inset -8px 0 4px -4px use-rgba(n-1, 0.16); + } + @include theme-dark { + box-shadow: inset -8px 0 4px -4px use-rgba(n-9, 0.75); + } + } + + &:before { + @include vertical-dashed-line(1px, 'primary'); + } + } + } + } +} diff --git a/src/table/new/table.component.scss b/src/table/new/table.component.scss new file mode 100644 index 000000000..0dae3b835 --- /dev/null +++ b/src/table/new/table.component.scss @@ -0,0 +1,212 @@ +@import '../../theme/var'; +@import '../../theme/mixin'; + +.aui-table { + display: block; + padding: 0 $table-padding $table-padding; + @include text-set(m, main); + + background-color: use-rgb(n-9); + border-radius: use-var(border-radius-l); + + &__row, + &__header-row { + display: flex; + align-items: center; + + &.hasPanel { + flex-wrap: wrap; + } + } + + &__header-row + &__row { + border-top-left-radius: use-var(border-radius-l); + border-top-right-radius: use-var(border-radius-l); + } + + &__row { + position: relative; + border-width: 1px; + border-style: solid; + border-color: use-rgb(n-8); + border-bottom-width: 0; + background-color: use-rgb(n-10); + padding: 0 ($table-cell-padding-h - 1); + min-height: $table-row-min-height; + box-sizing: content-box; + + &:first-child { + border-top-left-radius: use-var(border-radius-l); + border-top-right-radius: use-var(border-radius-l); + } + + &:last-child { + border-bottom-width: 1px; + min-height: $table-row-min-height; + border-bottom-left-radius: use-var(border-radius-l); + border-bottom-right-radius: use-var(border-radius-l); + } + + &.isDisabled:before { + content: ''; + z-index: 2; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: use-rgb(n-10); + opacity: 0.7; + cursor: not-allowed; + } + } + + &__header-row { + background-color: use-rgb(n-9); + padding: 0 $table-cell-padding-h; + } + + &__cell, + &__header-cell { + display: flex; + align-items: center; + flex: 1; + } + + &__cell { + padding: $table-cell-padding-v $table-cell-padding-h; + background-color: use-rgb(n-10); + overflow: hidden; + + &--column { + flex-direction: column; + justify-content: center; + align-items: flex-start; + } + } + + &__header-cell { + padding: $table-padding $table-cell-padding-h; + font-weight: use-var(font-weight-bold); + background-color: use-rgb(n-9); + @include text-overflow; + } + + &__column-expand-button { + display: flex; + align-items: center; + max-width: calc(#{$table-cell-padding-h} * 2 + #{use-var(icon-size-m)}); + + &.aui-table__cell { + height: $table-row-min-height; + } + + .aui-expand-button { + @include expand-button; + } + } + + &__column-expand-panel { + margin-top: -6px; + + &.aui-table__header-cell { + display: none; + } + + &.aui-table__cell { + width: 100%; + flex-shrink: 0; + flex-basis: 100%; + padding: 0 $table-cell-padding-h; + overflow: hidden; + + .aui-table__cell-expand-panel { + width: 100%; + border-radius: use-var(border-radius-l); + overflow: hidden; + + &-content { + &.hasBackground { + padding: 16px; + background-color: use-rgb(n-9); + } + } + } + } + } +} + +// 作为table组件,使用原生table是常见的,所以提供内置的样式适配 +table.aui-table { + display: table; + width: 100%; + border-spacing: 0; + border-collapse: separate; + + .aui-table { + &__row, + &__header-row { + display: table-row; + } + + &__cell, + &__header-cell { + display: table-cell; + } + + &__cell { + border-bottom-width: 1px; + border-bottom-style: solid; + border-color: use-rgb(n-8); + } + } + + tbody { + tr:first-child td { + border-top-width: 1px; + border-top-style: solid; + + &:first-child { + border-top-left-radius: use-var(border-radius-l); + } + + &:last-child { + border-top-right-radius: use-var(border-radius-l); + } + } + + tr td:first-child { + border-left-width: 1px; + border-left-style: solid; + } + + tr td:last-child { + border-right-width: 1px; + border-right-style: solid; + } + + tr:last-child td { + &:first-child { + border-bottom-left-radius: use-var(border-radius-l); + } + + &:last-child { + border-bottom-right-radius: use-var(border-radius-l); + } + } + + td { + vertical-align: middle; + } + } + + thead { + th { + text-align: left; + } + } + + [rowspan='0'] { + display: none; + } +} diff --git a/src/table/new/table.component.ts b/src/table/new/table.component.ts new file mode 100644 index 000000000..dc6e2b277 --- /dev/null +++ b/src/table/new/table.component.ts @@ -0,0 +1,102 @@ +import { + _DisposeViewRepeaterStrategy, + _VIEW_REPEATER_STRATEGY, +} from '@angular/cdk/collections'; +import { + CDK_TABLE, + CDK_TABLE_TEMPLATE, + CdkTable, + _COALESCED_STYLE_SCHEDULER, + _CoalescedStyleScheduler, +} from '@angular/cdk/table'; +import { + AfterContentInit, + ChangeDetectionStrategy, + Component, + ContentChild, + Input, + OnDestroy, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { + TablePlaceholderDefDirective, + TablePlaceholderOutletDirective, +} from '../table-placeholder.directive'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'table[auiTable]', + exportAs: 'auiTable', + encapsulation: ViewEncapsulation.None, + styleUrls: ['table.component.scss', 'table-scroll.scss'], + template: + CDK_TABLE_TEMPLATE + + '', + host: { + class: 'aui-table', + }, + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: CDK_TABLE, + useExisting: NewTableComponent, + }, + { + provide: _VIEW_REPEATER_STRATEGY, + useClass: _DisposeViewRepeaterStrategy, + }, + { + provide: _COALESCED_STYLE_SCHEDULER, + useClass: _CoalescedStyleScheduler, + }, + ], +}) +export class NewTableComponent + extends CdkTable + implements AfterContentInit, OnDestroy +{ + @Input() + enableScrollWrapper: boolean; + + @ViewChild(TablePlaceholderOutletDirective, { static: true }) + _placeholderOutlet: TablePlaceholderOutletDirective; + + @ContentChild(TablePlaceholderDefDirective, { static: true }) + _placeholderDef: TablePlaceholderDefDirective; + + // FIXME: workaround to override because it will break constructor if it is field, but why MatTable works? + // @ts-ignore + protected get stickyCssClass() { + return 'aui-table-sticky'; + } + + protected override set stickyCssClass(_: string) { + // nothing + } + + ngAfterContentInit() { + this._createPlaceholder(); + } + + private _createPlaceholder() { + const footerRow = this._placeholderDef; + if (!this._placeholderDef) { + return; + } + + const container = this._placeholderOutlet.viewContainer; + container.createEmbeddedView(footerRow.templateRef); + } + + private _clearPlaceholder() { + this._placeholderOutlet.viewContainer.clear(); + } + + override ngOnDestroy() { + super.ngOnDestroy(); + this._clearPlaceholder(); + } +} diff --git a/src/table/table.module.ts b/src/table/table.module.ts index a17aeaa11..90d26fc1c 100644 --- a/src/table/table.module.ts +++ b/src/table/table.module.ts @@ -4,6 +4,11 @@ import { NgModule } from '@angular/core'; import { IconModule } from '../icon/icon.module'; +import { NewTableCellDirective } from './new/table-cell.directive'; +import { NewTableHeaderCellDirective } from './new/table-header-cell.directive'; +import { NewTableHeaderRowComponent } from './new/table-header-row.component'; +import { NewTableRowComponent } from './new/table-row.component'; +import { NewTableComponent } from './new/table.component'; import { TableCellDefDirective } from './table-cell-def.directive'; import { TableExpandButtonCellComponent, @@ -35,6 +40,9 @@ import { TableComponent } from './table.component'; TableHeaderRowComponent, TableExpandButtonCellComponent, TableExpandPanelCellComponent, + NewTableComponent, + NewTableHeaderRowComponent, + NewTableRowComponent, TableCellDirective, TableCellDefDirective, TableHeaderCellDirective, @@ -46,6 +54,8 @@ import { TableComponent } from './table.component'; TablePlaceholderOutletDirective, TablePlaceholderDefDirective, TableScrollWrapperDirective, + NewTableCellDirective, + NewTableHeaderCellDirective, ], exports: [ TableComponent, @@ -53,6 +63,9 @@ import { TableComponent } from './table.component'; TableHeaderRowComponent, TableExpandButtonCellComponent, TableExpandPanelCellComponent, + NewTableComponent, + NewTableHeaderRowComponent, + NewTableRowComponent, TableCellDirective, TableCellDefDirective, TableHeaderCellDirective, @@ -64,6 +77,8 @@ import { TableComponent } from './table.component'; TablePlaceholderOutletDirective, TablePlaceholderDefDirective, TableScrollWrapperDirective, + NewTableCellDirective, + NewTableHeaderCellDirective, ], }) export class TableModule {} diff --git a/stories/table/table.stories.mdx b/stories/table/table.stories.mdx index b382f45c2..11e7696e4 100644 --- a/stories/table/table.stories.mdx +++ b/stories/table/table.stories.mdx @@ -54,20 +54,34 @@ Table 组件基于 CdkTable 开发, 加了一些 alauda 自定义的表格样式 > {{ template: /* HTML */ ` - + - + + - + - + + - - + - + > +
No. - - +
{{ item.id }}
- +
Name - - + @@ -75,23 +89,31 @@ Table 组件基于 CdkTable 开发, 加了一些 alauda 自定义的表格样式
{{ item.displayName }}
- +
Value - - + {{ item.value }} - +
`, props: { dataSource: DATA_SOURCE,