Browse Source

UI: Implement pagination support on overflow for toggle select/header component.

pull/9033/head
Igor Kulikov 3 years ago
parent
commit
20db421a8a
  1. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  2. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  3. 33
      ui-ngx/src/app/shared/components/toggle-header.component.html
  4. 26
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  5. 179
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  6. 1
      ui-ngx/src/app/shared/components/toggle-select.component.html
  7. 9
      ui-ngx/src/app/shared/components/toggle-select.component.ts

2
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -20,7 +20,7 @@
<h2 translate>widget.add</h2> <h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span> <span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}"> <tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" disablePagination [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option> <tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option> <tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -360,7 +360,7 @@
[isReadOnly]="true" [isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()"> (closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" [(ngModel)]="tbEditWidget.widgetConfigMode"> <tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" disablePagination [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option> <tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option> <tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>

33
ui-ngx/src/app/shared/components/toggle-header.component.html

@ -15,14 +15,31 @@
limitations under the License. limitations under the License.
--> -->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" <button mat-icon-button
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'), [disabled]="!leftPaginationEnabled"
'tb-invert': appearance === 'fill-invert', (click)="handlePaginatorClick('before', $event)"
'tb-ignore-md-lg': ignoreMdLgSize, (touchstart)="handlePaginatorTouchStart('before', $event)"
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value" class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': (ignoreMdLgSize || !isMdLg), 'tb-mat-24': !ignoreMdLgSize && isMdLg}">
(ngModelChange)="valueChange.emit(value)"> <mat-icon>chevron_left</mat-icon>
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle> </button>
</mat-button-toggle-group> <div #toggleGroupContainer class="tb-toggle-container" [class]="{'tb-disable-pagination': disablePagination}" *ngIf="!isMdLg || !useSelectOnMdLg; else select" >
<mat-button-toggle-group #toggleGroup
class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize,
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<button mat-icon-button
[disabled]="!rightPaginationEnabled"
(click)="handlePaginatorClick('after', $event)"
(touchstart)="handlePaginatorTouchStart('after', $event)"
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': (ignoreMdLgSize || !isMdLg), 'tb-mat-24': !ignoreMdLgSize && isMdLg}">
<mat-icon>chevron_right</mat-icon>
</button>
<ng-template #select> <ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled"> <mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled">

26
ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -17,8 +17,34 @@
@import "../../../theme"; @import "../../../theme";
@import "../../../scss/constants"; @import "../../../scss/constants";
:host {
max-width: 100%;
display: grid;
grid-template-columns: min-content minmax(auto, 1fr) min-content;
.tb-toggle-header-pagination-button {
display: none;
}
&.tb-toggle-header-pagination-controls-enabled {
.tb-toggle-header-pagination-button {
display: block;
}
}
.tb-toggle-container {
display: inline-grid;
grid-column: 2;
overflow: hidden;
&.tb-disable-pagination {
overflow: visible;
}
}
.tb-toggle-header {
transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
}
}
:host ::ng-deep { :host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
overflow: visible;
width: 100%; width: 100%;
border-radius: 100px; border-radius: 100px;
height: 32px; height: 32px;

179
ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -15,18 +15,22 @@
/// ///
import { import {
AfterContentChecked,
AfterContentInit, AfterContentInit,
AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ContentChildren, ContentChildren,
Directive, Directive,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
QueryList QueryList,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -36,6 +40,8 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { startWith, takeUntil } from 'rxjs/operators'; import { startWith, takeUntil } from 'rxjs/operators';
import { Platform } from '@angular/cdk/platform';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
export interface ToggleHeaderOption { export interface ToggleHeaderOption {
name: string; name: string;
@ -44,6 +50,8 @@ export interface ToggleHeaderOption {
export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
export type ScrollDirection = 'after' | 'before';
@Directive( @Directive(
{ {
// eslint-disable-next-line @angular-eslint/directive-selector // eslint-disable-next-line @angular-eslint/directive-selector
@ -72,7 +80,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
@Input() @Input()
options: ToggleHeaderOption[] = []; options: ToggleHeaderOption[] = [];
private _destroyed = new Subject<void>(); protected _destroyed = new Subject<void>();
protected constructor(protected store: Store<AppState>) { protected constructor(protected store: Store<AppState>) {
super(store); super(store);
@ -109,7 +117,34 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
templateUrl: './toggle-header.component.html', templateUrl: './toggle-header.component.html',
styleUrls: ['./toggle-header.component.scss'] styleUrls: ['./toggle-header.component.scss']
}) })
export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterContentInit, OnDestroy { export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked, OnDestroy {
@ViewChild('toggleGroup', {static: false})
toggleGroup: ElementRef<HTMLElement>;
@ViewChild(MatButtonToggleGroup, {static: false})
buttonToggleGroup: MatButtonToggleGroup;
@ViewChild('toggleGroupContainer', {static: false})
toggleGroupContainer: ElementRef<HTMLElement>;
@HostBinding('class.tb-toggle-header-pagination-controls-enabled')
private showPaginationControls = false;
private toggleGroupResize$: ResizeObserver;
leftPaginationEnabled = false;
rightPaginationEnabled = false;
private _scrollDistance = 0;
private _scrollDistanceChanged: boolean;
get scrollDistance(): number {
return this._scrollDistance;
}
set scrollDistance(value: number) {
this._scrollTo(value);
}
@Input() @Input()
value: any; value: any;
@ -120,6 +155,10 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
@Input() @Input()
name: string; name: string;
@Input()
@coerceBoolean()
disablePagination = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
useSelectOnMdLg = true; useSelectOnMdLg = true;
@ -141,6 +180,7 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private platform: Platform,
private breakpointObserver: BreakpointObserver) { private breakpointObserver: BreakpointObserver) {
super(store); super(store);
} }
@ -154,9 +194,142 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
this.cd.markForCheck(); this.cd.markForCheck();
} }
); );
if (!this.disablePagination) {
this.valueChange.pipe(takeUntil(this._destroyed)).subscribe(() => {
this.scrollToToggleOptionValue();
});
}
}
ngOnDestroy() {
if (this.toggleGroupResize$) {
this.toggleGroupResize$.disconnect();
}
super.ngOnDestroy();
}
ngAfterViewInit() {
if (!this.disablePagination && !this.useSelectOnMdLg) {
this.toggleGroupResize$ = new ResizeObserver(() => {
this.updatePagination();
});
this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement);
}
}
ngAfterContentChecked() {
if (this._scrollDistanceChanged) {
this.updateToggleHeaderScrollPosition();
this._scrollDistanceChanged = false;
this.cd.markForCheck();
}
} }
trackByHeaderOption(index: number, option: ToggleHeaderOption){ trackByHeaderOption(index: number, option: ToggleHeaderOption){
return option.value; return option.value;
} }
handlePaginatorClick(direction: ScrollDirection, $event: Event) {
if ($event) {
$event.stopPropagation();
}
this.scrollHeader(direction);
}
handlePaginatorTouchStart(direction: ScrollDirection, $event: Event) {
if (direction === 'before' && !this.leftPaginationEnabled ||
direction === 'after' && !this.rightPaginationEnabled) {
$event.preventDefault();
}
}
private scrollHeader(direction: ScrollDirection) {
const viewLength = this.toggleGroup.nativeElement.offsetWidth;
// Move the scroll distance one-third the length of the tab list's viewport.
const scrollAmount = ((direction === 'before' ? -1 : 1) * viewLength) / 3;
return this._scrollTo(this._scrollDistance + scrollAmount);
}
private scrollToToggleOptionValue() {
if (this.buttonToggleGroup && this.buttonToggleGroup.selected) {
const selectedToggleButton = this.buttonToggleGroup.selected as MatButtonToggle;
const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth;
const {offsetLeft, offsetWidth} = (selectedToggleButton._buttonElement.nativeElement.offsetParent as HTMLElement);
const labelBeforePos = offsetLeft; // this.toggleGroup.nativeElement.offsetWidth - offsetLeft;
const labelAfterPos = labelBeforePos + offsetWidth;
const beforeVisiblePos = this.scrollDistance;
const afterVisiblePos = this.scrollDistance + viewLength;
if (labelBeforePos < beforeVisiblePos) {
this.scrollDistance -= beforeVisiblePos - labelBeforePos;
} else if (labelAfterPos > afterVisiblePos) {
this.scrollDistance += Math.min(
labelAfterPos - afterVisiblePos,
labelBeforePos - beforeVisiblePos,
);
}
}
}
private updatePagination() {
this.checkPaginationEnabled();
this.checkPaginationControls();
this.updateToggleHeaderScrollPosition();
}
private checkPaginationEnabled() {
if (this.toggleGroupContainer) {
const isEnabled = this.toggleGroup.nativeElement.scrollWidth > this.toggleGroupContainer.nativeElement.offsetWidth;
if (isEnabled !== this.showPaginationControls) {
if (!isEnabled) {
this.scrollDistance = 0;
} else {
setTimeout(() => {
this.scrollToToggleOptionValue();
}, 0);
}
this.cd.markForCheck();
this.showPaginationControls = isEnabled;
}
} else {
this.showPaginationControls = false;
}
}
private checkPaginationControls() {
if (!this.showPaginationControls) {
this.leftPaginationEnabled = this.rightPaginationEnabled = false;
} else {
// Check if the pagination arrows should be activated.
this.leftPaginationEnabled = this.scrollDistance > 0;
this.rightPaginationEnabled = this.scrollDistance < this.getMaxScrollDistance();
this.cd.markForCheck();
}
}
private getMaxScrollDistance(): number {
const lengthOfToggleGroup = this.toggleGroup.nativeElement.scrollWidth;
const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth;
return lengthOfToggleGroup - viewLength || 0;
}
private _scrollTo(position: number) {
if (!this.showPaginationControls) {
return {maxScrollDistance: 0, distance: 0};
} else {
const maxScrollDistance = this.getMaxScrollDistance();
this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));
this._scrollDistanceChanged = true;
this.checkPaginationControls();
return {maxScrollDistance, distance: this._scrollDistance};
}
}
private updateToggleHeaderScrollPosition() {
const scrollDistance = this.scrollDistance;
const translateX = -scrollDistance;
this.toggleGroup.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`;
if (this.platform.TRIDENT || this.platform.EDGE) {
this.toggleGroupContainer.nativeElement.scrollLeft = 0;
}
}
} }

1
ui-ngx/src/app/shared/components/toggle-select.component.html

@ -20,6 +20,7 @@
useSelectOnMdLg="false" useSelectOnMdLg="false"
[disabled]="disabled" [disabled]="disabled"
[appearance]="appearance" [appearance]="appearance"
[disablePagination]="disablePagination"
[options]="options" [options]="options"
[value]="modelValue" [value]="modelValue"
(valueChange)="updateModel($event)"> (valueChange)="updateModel($event)">

9
ui-ngx/src/app/shared/components/toggle-select.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input } from '@angular/core'; import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -35,6 +35,9 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}) })
export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor { export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor {
@HostBinding('style.maxWidth')
get maxWidth() { return '100%'; }
@Input() @Input()
@coerceBoolean() @coerceBoolean()
disabled: boolean; disabled: boolean;
@ -42,6 +45,10 @@ export class ToggleSelectComponent extends _ToggleBase implements ControlValueAc
@Input() @Input()
appearance: ToggleHeaderAppearance = 'stroked'; appearance: ToggleHeaderAppearance = 'stroked';
@Input()
@coerceBoolean()
disablePagination = false;
modelValue: any; modelValue: any;
private propagateChange = null; private propagateChange = null;

Loading…
Cancel
Save