1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[CL-879] use tooltip on icon button (#16576)

* Add tooltip to icon button to display label

* remove legacy cdr variable

* create overlay on focus or hover

* attach describdedby ids

* fix type errors

* remove aria-describedby when not necessary

* fix failing tests

* implement Claude feedback

* fixing broken specs

* remove host attr binding

* Simplify directive aria logic

* Move id to statis number

* do not render empty tooltip

* pass id to tooltip component

* remove pointer-events none to allow tooltip on normal buttons

* exclude some tooltip stories

* change describedby input name

* add story with tooltip on regular button

* enhanced tooltip docs

* set model directly

* change model to input
This commit is contained in:
Bryan Cunningham
2025-10-29 09:49:16 -04:00
committed by GitHub
parent 460d66d624
commit 5b815c4ae4
11 changed files with 137 additions and 48 deletions

View File

@@ -8,8 +8,9 @@ import {
ElementRef,
Injector,
input,
effect,
signal,
model,
computed,
} from "@angular/core";
import { TooltipPositionIdentifier, tooltipPositions } from "./tooltip-positions";
@@ -26,30 +27,39 @@ import { TooltipComponent, TOOLTIP_DATA } from "./tooltip.component";
"(mouseleave)": "hideTooltip()",
"(focus)": "showTooltip()",
"(blur)": "hideTooltip()",
"[attr.aria-describedby]": "resolvedDescribedByIds()",
},
})
export class TooltipDirective implements OnInit {
private static nextId = 0;
/**
* The value of this input is forwarded to the tooltip.component to render
*/
readonly bitTooltip = input.required<string>();
readonly tooltipContent = model("", { alias: "bitTooltip" });
/**
* The value of this input is forwarded to the tooltip.component to set its position explicitly.
* @default "above-center"
*/
readonly tooltipPosition = input<TooltipPositionIdentifier>("above-center");
/**
* Input so the consumer can choose to add the tooltip id to the aria-describedby attribute of the host element.
*/
readonly addTooltipToDescribedby = input<boolean>(false);
private readonly isVisible = signal(false);
private overlayRef: OverlayRef | undefined;
private elementRef = inject(ElementRef);
private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private overlay = inject(Overlay);
private viewContainerRef = inject(ViewContainerRef);
private injector = inject(Injector);
private positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withFlexibleDimensions(false)
.withPush(true);
private tooltipId = `bit-tooltip-${TooltipDirective.nextId++}`;
private currentDescribedByIds =
this.elementRef.nativeElement.getAttribute("aria-describedby") || null;
private tooltipPortal = new ComponentPortal(
TooltipComponent,
@@ -59,23 +69,50 @@ export class TooltipDirective implements OnInit {
{
provide: TOOLTIP_DATA,
useValue: {
content: this.bitTooltip,
content: this.tooltipContent,
isVisible: this.isVisible,
tooltipPosition: this.tooltipPosition,
id: signal(this.tooltipId),
},
},
],
}),
);
private destroyTooltip = () => {
this.overlayRef?.dispose();
this.overlayRef = undefined;
this.isVisible.set(false);
};
private showTooltip = () => {
if (!this.overlayRef) {
this.overlayRef = this.overlay.create({
...this.defaultPopoverConfig,
positionStrategy: this.positionStrategy,
});
this.overlayRef.attach(this.tooltipPortal);
}
this.isVisible.set(true);
};
private hideTooltip = () => {
this.isVisible.set(false);
this.destroyTooltip();
};
private readonly resolvedDescribedByIds = computed(() => {
if (this.addTooltipToDescribedby()) {
if (this.currentDescribedByIds) {
return `${this.currentDescribedByIds || ""} ${this.tooltipId}`;
} else {
return this.tooltipId;
}
} else {
return this.currentDescribedByIds;
}
});
private computePositions(tooltipPosition: TooltipPositionIdentifier) {
const chosenPosition = tooltipPositions.find((position) => position.id === tooltipPosition);
@@ -91,20 +128,5 @@ export class TooltipDirective implements OnInit {
ngOnInit() {
this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition()));
this.overlayRef = this.overlay.create({
...this.defaultPopoverConfig,
positionStrategy: this.positionStrategy,
});
this.overlayRef.attach(this.tooltipPortal);
effect(
() => {
this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition()));
this.overlayRef?.updatePosition();
},
{ injector: this.injector },
);
}
}