From 8afc94cfc97728cc8b58723511de90980a40d036 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 20:23:45 +0300 Subject: [PATCH] feat(chip-overflow): optional showOverflowedTitle native tooltip - Add a `showOverflowedTitle` boolean input (defaults to false, coerceBoolean-decorated) to ChipOverflowDirective. When true and hiddenCount > 0, the overflow `+N` chip gets a `title` attribute set to the comma-joined trimmed textContent of every overflowing chip, surfacing them via the native browser tooltip. When the flag is off, no items overflow, or hiddenCount drops back to zero, the title attribute is removed. - Keep `pointer-events` in sync with the title so the native tooltip actually fires: remove `pointer-events: none` while a title is present; reapply it otherwise (matches the initial state set in createOverflowChip). - Opt the IoT Hub item detail dialog's three tb-chip-overflow meta-chip groups (connectivity, categories, use-cases) into showOverflowedTitle so hovering the `+N` chip lists the hidden values. --- .../iot-hub-item-detail-dialog.component.html | 6 ++--- .../directives/chip-overflow.directive.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 6c0318abbc..3827599242 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -203,7 +203,7 @@ @if (item.dataDescriptor?.connectivity?.length) {
{{ 'iot-hub.connectivity' | translate }} -
+
@for (conn of item.dataDescriptor.connectivity; track conn) { {{ conn }} } @@ -214,7 +214,7 @@ @if (item.categories?.length) {
{{ 'iot-hub.category' | translate }} -
+
@for (cat of item.categories; track cat) { {{ cat }} } @@ -225,7 +225,7 @@ @if (item.useCases?.length) {
{{ 'iot-hub.use-cases' | translate }} -
+
@for (uc of item.useCases; track uc) { {{ uc }} } diff --git a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts index 7ed84f10e7..10585f1453 100644 --- a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts +++ b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts @@ -15,6 +15,7 @@ /// import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy, Renderer2 } from '@angular/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Directive({ selector: '[tb-chip-overflow]', @@ -28,6 +29,10 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { @Input() overflowClass = 'tb-overflow-chip'; @Input() minChips = 1; + @Input() + @coerceBoolean() + showOverflowedTitle = false; + private resizeObserver?: ResizeObserver; private mutationObserver?: MutationObserver; private overflowEl!: HTMLElement; @@ -151,6 +156,7 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { // 4. Determine which chips fit let usedWidth = 0; let hiddenCount = 0; + const hiddenChips: HTMLElement[] = []; const minChips = Math.max(1, this.minChips); for (let i = 0; i < chips.length; i++) { @@ -163,12 +169,29 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { usedWidth = nextUsedWidth; } else { hiddenCount++; + hiddenChips.push(chips[i]); } } if (hiddenCount > 0) { this.renderer.setProperty(this.overflowEl, 'textContent', this.overflowTemplate.replace('{n}', String(hiddenCount))); this.renderer.setStyle(this.overflowEl, 'display', 'inline-flex'); + if (this.showOverflowedTitle) { + const title = hiddenChips + .map(chip => (chip.textContent || '').trim()) + .filter(text => !!text) + .join(', '); + this.renderer.setAttribute(this.overflowEl, 'title', title); + // Allow pointer interaction so the browser's native title + // tooltip appears on hover. + this.renderer.removeStyle(this.overflowEl, 'pointerEvents'); + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); + } + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); } } }