UI: Error icon with tooltip for toggle select

This commit is contained in:
Artem Dzhereleiko 2025-04-02 14:08:28 +03:00
parent 052bfc09dd
commit d3e20161c9
5 changed files with 62 additions and 11 deletions

View File

@ -41,10 +41,10 @@
</div> </div>
<tb-toggle-select [(ngModel)]="dataLayerMode" <tb-toggle-select [(ngModel)]="dataLayerMode"
[ngModelOptions]="{ standalone: true }"> [ngModelOptions]="{ standalone: true }">
<tb-toggle-option *ngIf="trip" value="trips">{{ 'widgets.maps.overlays.trips' | translate }}</tb-toggle-option> <tb-toggle-option *ngIf="trip" value="trips" [error]="mapSettingsFormGroup.get('trips').invalid" errorText="widgets.maps.overlays.required-fields">{{ 'widgets.maps.overlays.trips' | translate }}</tb-toggle-option>
<tb-toggle-option value="markers">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option> <tb-toggle-option value="markers" [error]="mapSettingsFormGroup.get('markers').invalid" errorText="widgets.maps.overlays.required-fields">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option> <tb-toggle-option value="polygons" [error]="mapSettingsFormGroup.get('polygons').invalid" errorText="widgets.maps.overlays.required-fields">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option> <tb-toggle-option value="circles" [error]="mapSettingsFormGroup.get('circles').invalid" errorText="widgets.maps.overlays.required-fields">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-map-data-layers *ngIf="trip" <tb-map-data-layers *ngIf="trip"

View File

@ -41,7 +41,16 @@
[name]="name" [name]="name"
[(ngModel)]="value" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(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 *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">
{{ option.name }}
<mat-icon matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="option.errorText | translate"
*ngIf="option.error && value !== option.value"
class="tb-error tb-error-icon">
warning
</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
<button mat-icon-button <button mat-icon-button

View File

@ -43,6 +43,12 @@
.tb-toggle-header { .tb-toggle-header {
transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
} }
.tb-error-icon {
width: 12px;
height: 12px;
font-size: 12px;
}
} }
:host ::ng-deep { :host ::ng-deep {

View File

@ -16,7 +16,8 @@
import { import {
AfterContentChecked, AfterContentChecked,
AfterContentInit, AfterViewChecked, AfterContentInit,
AfterViewChecked,
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@ -25,11 +26,14 @@ import {
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, NgZone, Input,
NgZone,
OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
QueryList, QueryList,
SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -42,10 +46,13 @@ 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 { Platform } from '@angular/cdk/platform';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
import { isDefinedAndNotNull } from '@core/utils';
export interface ToggleHeaderOption { export interface ToggleHeaderOption {
name: string; name: string;
value: any; value: any;
error?: boolean;
errorText?: any;
} }
export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
@ -59,10 +66,16 @@ export type ScrollDirection = 'after' | 'before';
} }
) )
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
export class ToggleOption { export class ToggleOption implements OnChanges {
@Input() value: any; @Input() value: any;
@Input() error: boolean;
@Input() errorText: any;
@Output() errorChange = new EventEmitter<boolean>();
get viewValue(): string { get viewValue(): string {
return (this._element?.nativeElement.textContent || '').trim(); return (this._element?.nativeElement.textContent || '').trim();
} }
@ -70,6 +83,14 @@ export class ToggleOption {
constructor( constructor(
private _element: ElementRef<HTMLElement> private _element: ElementRef<HTMLElement>
) {} ) {}
ngOnChanges(changes: SimpleChanges) {
if (changes['error']) {
if (!changes['error'].firstChange && changes['error'].currentValue !== changes['error'].previousValue) {
this.errorChange.emit(this.error);
}
}
}
} }
@Directive() @Directive()
@ -88,6 +109,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.toggleOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { this.toggleOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
this.subscribeToToggleOptions();
this.syncToggleHeaderOptions(); this.syncToggleHeaderOptions();
}); });
} }
@ -97,13 +119,26 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
this._destroyed.complete(); this._destroyed.complete();
} }
private subscribeToToggleOptions() {
this.toggleOptions.forEach(option => {
if (isDefinedAndNotNull(option.error) || isDefinedAndNotNull(option.errorText)) {
option.errorChange.pipe(takeUntil(this._destroyed)).subscribe(() => {
this.syncToggleHeaderOptions();
});
}
});
}
private syncToggleHeaderOptions() { private syncToggleHeaderOptions() {
if (this.toggleOptions?.length) { if (this.toggleOptions?.length) {
this.options.length = 0; this.options.length = 0;
this.toggleOptions.forEach(option => { this.toggleOptions.forEach(option => {
this.options.push( this.options.push(
{ name: option.viewValue, {
value: option.value name: option.viewValue,
value: option.value,
error: option.error,
errorText: option.errorText
} }
); );
}); });

View File

@ -7980,7 +7980,8 @@
"trips": "Trips", "trips": "Trips",
"markers": "Markers", "markers": "Markers",
"polygons": "Polygons", "polygons": "Polygons",
"circles": "Circles" "circles": "Circles",
"required-fields": "Required fields are not filled in."
}, },
"data-layer": { "data-layer": {
"source": "Source", "source": "Source",