ng-hub-ui-utils 22.12.0 → 22.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -25
- package/fesm2022/ng-hub-ui-utils.mjs +109 -3
- package/fesm2022/ng-hub-ui-utils.mjs.map +1 -1
- package/package.json +13 -9
- package/styles/tooltip.scss +5 -1
- package/types/ng-hub-ui-utils.d.ts +34 -0
package/README.md
CHANGED
|
@@ -116,8 +116,8 @@ export class ExampleComponent {
|
|
|
116
116
|
overlayY: 'top'
|
|
117
117
|
}]);
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
119
|
+
// Render a component (or a TemplateRef) into the overlay; you get the host element back
|
|
120
|
+
const overlayElement = overlayRef.attach(MyComponent);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
```
|
|
@@ -128,9 +128,11 @@ export class ExampleComponent {
|
|
|
128
128
|
this.overlayService.create({ zIndex: 1100 }); // OverlayConfig.zIndex: number | string — takes precedence over the token
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
### 🎯 Popup Service
|
|
131
|
+
### 🎯 Popup Service
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
`PopupService<T>` hosts a dynamically created popup component and runs its show/hide
|
|
134
|
+
transition. It is a concrete class — subclass it when the popup needs its own API, as
|
|
135
|
+
below, or provide it through a factory.
|
|
134
136
|
|
|
135
137
|
```typescript
|
|
136
138
|
import { PopupService } from 'ng-hub-ui-utils';
|
|
@@ -356,16 +358,17 @@ import { GetPipe, UcfirstPipe } from 'ng-hub-ui-utils';
|
|
|
356
358
|
|
|
357
359
|
### 🏷️ Tooltip Directive
|
|
358
360
|
|
|
359
|
-
Add a lightweight, themeable tooltip to any element with the `
|
|
360
|
-
The tooltip is appended to `<body>` (never clipped) and shows on
|
|
361
|
+
Add a lightweight, themeable tooltip to any element with the `HubTooltipDirective`
|
|
362
|
+
(`[hubTooltip]`). The tooltip is appended to `<body>` (never clipped) and shows on
|
|
363
|
+
hover/focus.
|
|
361
364
|
|
|
362
365
|
```typescript
|
|
363
|
-
import {
|
|
366
|
+
import { HubTooltipDirective } from 'ng-hub-ui-utils';
|
|
364
367
|
|
|
365
368
|
@Component({
|
|
366
369
|
standalone: true,
|
|
367
|
-
imports: [
|
|
368
|
-
template: `<button
|
|
370
|
+
imports: [HubTooltipDirective],
|
|
371
|
+
template: `<button hubTooltip="Save changes" hubTooltipPlacement="top">Save</button>`
|
|
369
372
|
})
|
|
370
373
|
export class ExampleComponent {}
|
|
371
374
|
```
|
|
@@ -378,8 +381,24 @@ export class ExampleComponent {}
|
|
|
378
381
|
> @use 'ng-hub-ui-utils/styles/tooltip';
|
|
379
382
|
> ```
|
|
380
383
|
|
|
381
|
-
Inputs: `
|
|
382
|
-
`
|
|
384
|
+
Inputs: `hubTooltip` (text), `hubTooltipPlacement` (`top` | `bottom` | `left` | `right`,
|
|
385
|
+
default `top`), `hubTooltipDelay` (fade ms, default `150`), `hubTooltipOffset` (px, default `8`).
|
|
386
|
+
|
|
387
|
+
> **`TooltipDirective` (`[tooltip]`) is deprecated since 22.9.0.** It still works,
|
|
388
|
+
> unchanged — both directives are thin shells over the same `HubTooltipController` — but its
|
|
389
|
+
> bare input names (`tooltip`, `placement`, `delay`, `offset`) belong to every directive on
|
|
390
|
+
> the element that declares them, which is how it collided with `[hubDropdown]`'s own
|
|
391
|
+
> `placement` and with the `tooltip` input of `<hub-badge>`. Migration is attribute for
|
|
392
|
+
> attribute: `tooltip` → `hubTooltip`, `placement` → `hubTooltipPlacement`,
|
|
393
|
+
> `delay` → `hubTooltipDelay`, `offset` → `hubTooltipOffset`.
|
|
394
|
+
|
|
395
|
+
Show the label **only while the host is truncated** with `HubOverflowTooltipDirective`
|
|
396
|
+
(`[hubOverflowTooltip]`), which tracks truncation live with a `ResizeObserver` and a
|
|
397
|
+
`MutationObserver` and resolves its tooltip through `HUB_TOOLTIP_ADAPTER`:
|
|
398
|
+
|
|
399
|
+
```html
|
|
400
|
+
<span class="label" [hubOverflowTooltip]="item.label">{{ item.label }}</span>
|
|
401
|
+
```
|
|
383
402
|
|
|
384
403
|
Theme it from any scope with `--hub-tooltip-*` variables:
|
|
385
404
|
|
|
@@ -394,8 +413,14 @@ Theme it from any scope with `--hub-tooltip-*` variables:
|
|
|
394
413
|
|
|
395
414
|
Available tokens: `--hub-tooltip-bg`, `--hub-tooltip-color`, `--hub-tooltip-opacity`,
|
|
396
415
|
`--hub-tooltip-padding-x`, `--hub-tooltip-padding-y`, `--hub-tooltip-border-radius`,
|
|
397
|
-
`--hub-tooltip-font-size`, `--hub-tooltip-
|
|
398
|
-
`--hub-tooltip-
|
|
416
|
+
`--hub-tooltip-font-size`, `--hub-tooltip-font-weight`, `--hub-tooltip-line-height`,
|
|
417
|
+
`--hub-tooltip-max-width`, `--hub-tooltip-zindex`, `--hub-tooltip-transition-duration`,
|
|
418
|
+
`--hub-tooltip-shadow`, `--hub-tooltip-font-family`, `--hub-tooltip-white-space`,
|
|
419
|
+
`--hub-tooltip-text-align`.
|
|
420
|
+
|
|
421
|
+
The last two arrived in 22.10.0, for the tooltip that carries a sentence rather than a
|
|
422
|
+
name: they are set on the **host**, which is the only element you can reach, because the
|
|
423
|
+
bubble itself lives on `<body>`, outside every component's styles.
|
|
399
424
|
|
|
400
425
|
#### Tooltip adapter for other libraries (`hubTooltipAdapter`)
|
|
401
426
|
|
|
@@ -414,10 +439,26 @@ providers: [
|
|
|
414
439
|
];
|
|
415
440
|
```
|
|
416
441
|
|
|
442
|
+
Inside this package the same token works the other way round: `[hubOverflowTooltip]`
|
|
443
|
+
resolves its tooltip through `HUB_TOOLTIP_ADAPTER`, which defaults to `hubTooltipAdapter`.
|
|
444
|
+
Swap it app-wide, or for one subtree, with `provideHubTooltip()`:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import { provideHubTooltip, HubTooltipAdapter } from 'ng-hub-ui-utils';
|
|
448
|
+
|
|
449
|
+
const myTooltip: HubTooltipAdapter = {
|
|
450
|
+
attach(host, text, options) {
|
|
451
|
+
/* … returns a HubTooltipHandle with update(text) and destroy() */
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
providers: [provideHubTooltip(myTooltip)];
|
|
456
|
+
```
|
|
457
|
+
|
|
417
458
|
Also available: the imperative `HubTooltipController` (engine) and the
|
|
418
|
-
`HubTooltipAdapter` / `HubTooltipHandle` / `HubTooltipOptions`
|
|
419
|
-
ecosystem-wide
|
|
420
|
-
section.
|
|
459
|
+
`HubTooltipAdapter` / `HubTooltipHandle` / `HubTooltipOptions` / `HubTooltipPlacement`
|
|
460
|
+
types. See the ecosystem-wide
|
|
461
|
+
[Synergies & agnosticism](../../README.md#synergies--agnosticism) section.
|
|
421
462
|
|
|
422
463
|
## 🚀 Installation
|
|
423
464
|
|
|
@@ -505,6 +546,20 @@ interface HubTranslationConfig {
|
|
|
505
546
|
|
|
506
547
|
The configuration is also exposed through the `HUB_TRANSLATION_CONFIG` injection token for advanced scenarios.
|
|
507
548
|
|
|
549
|
+
### `HUB_TRANSLATION_PREFIX`
|
|
550
|
+
|
|
551
|
+
Injection token that scopes a library's lookups to a collision-safe `HUBUI.<LIBRARY>.*`
|
|
552
|
+
namespace. `TranslatePipe` resolves the prefixed key first and falls back to the bare key,
|
|
553
|
+
so a flat dictionary that predates the token keeps working untouched.
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
providers: [{ provide: HUB_TRANSLATION_PREFIX, useValue: 'HUBUI.TABLE' }];
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The adapter types are exported alongside it: `HubTranslationSource`,
|
|
560
|
+
`HubTranslationOverrides`, `HubTranslationAdapterConfig`, `HubTranslationAdapterFactory`
|
|
561
|
+
and the `HUB_TRANSLATION_SOURCE` token `provideHubTranslationAdapter()` registers.
|
|
562
|
+
|
|
508
563
|
### `HubTranslationService`
|
|
509
564
|
|
|
510
565
|
Injectable service that holds the active translations and notifies subscribers when they change.
|
|
@@ -584,6 +639,19 @@ These functions back the i18n system and are exported for direct use:
|
|
|
584
639
|
|
|
585
640
|
- `regExpEscape(text: string): string` - Escapes special characters for RegExp
|
|
586
641
|
- `removeAccents(str: string): string` - Removes accents from text
|
|
642
|
+
- `interpolateString(expr?: string, params?: any, templateMatcher?: RegExp): string` - Replaces `{{ token }}` placeholders
|
|
643
|
+
- `generateUniqueId(length: number): string` - Random alphanumeric id, for a DOM node that needs one
|
|
644
|
+
|
|
645
|
+
### Object Functions
|
|
646
|
+
|
|
647
|
+
- `equals(o1: any, o2: any): boolean` - Deep equality
|
|
648
|
+
- `getValue(target: any, key: string): any` - Reads a nested value by dot-notation key
|
|
649
|
+
- `isObject(item: any): boolean` - Whether the value is a non-array object
|
|
650
|
+
- `mergeDeep(target: any, source: any): any` - Recursive merge; the only deep object helper in the package
|
|
651
|
+
|
|
652
|
+
### Signal Utilities
|
|
653
|
+
|
|
654
|
+
- `debouncedSignal<T>(source: Signal<T>, delay?: number | Signal<number>): Signal<T>` - Mirrors a signal, delaying each change; the delay can itself be a signal
|
|
587
655
|
|
|
588
656
|
### DOM Functions
|
|
589
657
|
|
|
@@ -591,6 +659,10 @@ These functions back the i18n system and are exported for direct use:
|
|
|
591
659
|
- `reflow(element: HTMLElement): DOMRect` - Forces browser reflow
|
|
592
660
|
- `getActiveElement(root?: Document | ShadowRoot): Element | null` - Gets active element including Shadow DOM
|
|
593
661
|
|
|
662
|
+
### Accent Resolution
|
|
663
|
+
|
|
664
|
+
- `resolveHubAccent(value: string | null | undefined): string | null` - The "any colour" accent resolver shared across the family: a bareword becomes `var(--hub-sys-color-<name>, <name>)`, a literal `#hex` / `rgb()` / `oklch()` / `var()` passes through unchanged, and an empty value yields `null`
|
|
665
|
+
|
|
594
666
|
### Colour Functions
|
|
595
667
|
|
|
596
668
|
- `parseColor(value): HubRgb | null` - Parses hex (3/4/6/8), `rgb()`, `hsl()`, `oklch()`, `oklab()`, the 148 CSS named colours and `transparent`, in modern and legacy syntax. No DOM, so it runs under SSR. Returns `null` — never throws — for anything it cannot resolve, `var()` and `currentColor` included
|
|
@@ -621,6 +693,27 @@ These functions back the i18n system and are exported for direct use:
|
|
|
621
693
|
- `hubFocusTrap(zone, element, stopFocusTrap$, refocusOnClick?)` - Creates focus trap for modals/overlays
|
|
622
694
|
- `FOCUSABLE_ELEMENTS_SELECTOR: string` - CSS selector for focusable elements
|
|
623
695
|
|
|
696
|
+
### Drag and Drop
|
|
697
|
+
|
|
698
|
+
The engine-agnostic half of native HTML5 drag and drop, shared by the libraries that
|
|
699
|
+
implement it. The UI primitives — handle, placeholder and preview directives — stay in each
|
|
700
|
+
library, because their selectors and data models differ.
|
|
701
|
+
|
|
702
|
+
- `HubDragDropService` - Root-provided coordinator. A drag spans two component instances and the native `dataTransfer` payload is unreadable during `dragover`, so a shared service is the only reliable channel for what is being dragged and from where. Owners `register()` / `unregister()`; `begin()`, `setTarget()` and the readonly `active` / `target` / `isDragging` signals report the drag in progress. It coordinates state only — it never mutates your collections
|
|
703
|
+
- `moveItemInArray<T>(array, fromIndex, toIndex): void` / `transferArrayItem<T>(source, target, fromIndex, toIndex): void` / `copyArrayItem<T>(source, target, fromIndex, toIndex): void` - In-place array moves, mirroring the `@angular/cdk` helpers of the same names
|
|
704
|
+
- `clamp(value, max)`, `computeTargetIndex(...)`, `toAbsoluteIndex(...)`, `containsNode(...)` - Index arithmetic for sliced and nested lists
|
|
705
|
+
- `resolveDropPosition(...)` with `DropRect` and `DragAxis` - Where a pointer sits relative to an item: `'before'` or `'after'`, on a vertical, horizontal or grid axis
|
|
706
|
+
- `createNativeDragImage(...)` returning `DragImageResult` - Renders the drag preview the browser shows
|
|
707
|
+
- `createPointerDragSession(config: PointerDragSessionConfig): PointerDragSession` - Pointer Events fallback for touch, where native drag events are not delivered
|
|
708
|
+
- Types: `DropPosition`, `DragPointerMode`, `DragContainerRef<T>`, `ActiveDrag<T>`, `DragTarget<T>`, `DragRegistration`
|
|
709
|
+
|
|
710
|
+
### Directives
|
|
711
|
+
|
|
712
|
+
- `HubTooltipDirective` (`[hubTooltip]`) - Tooltip on hover/focus. Inputs: `hubTooltip`, `hubTooltipPlacement`, `hubTooltipDelay`, `hubTooltipOffset`
|
|
713
|
+
- `HubOverflowTooltipDirective` (`[hubOverflowTooltip]`) - Tooltip shown only while the host label is truncated. Inputs: `hubOverflowTooltip`, `placement`
|
|
714
|
+
- `TooltipDirective` (`[tooltip]`) - **Deprecated since 22.9.0**, kept working. Inputs: `tooltip`, `placement`, `delay`, `offset`
|
|
715
|
+
- `provideHubTooltip(adapter: HubTooltipAdapter)` and `HUB_TOOLTIP_ADAPTER` - Swap the implementation behind `[hubOverflowTooltip]`, app-wide or per subtree; defaults to `hubTooltipAdapter`
|
|
716
|
+
|
|
624
717
|
### Pipes
|
|
625
718
|
|
|
626
719
|
#### GetPipe
|
|
@@ -681,18 +774,37 @@ class OverlayService {
|
|
|
681
774
|
}
|
|
682
775
|
|
|
683
776
|
class OverlayRef {
|
|
684
|
-
|
|
777
|
+
// Renders a template or a component into the overlay and returns the host element,
|
|
778
|
+
// not a ComponentRef: the overlay owns the view it created and tears it down itself.
|
|
779
|
+
attach(content: TemplateRef<unknown> | Type<unknown>, viewContainerRef?: ViewContainerRef): HTMLElement;
|
|
685
780
|
detach(): void;
|
|
686
781
|
dispose(): void;
|
|
782
|
+
hasAttached(): boolean;
|
|
687
783
|
updatePosition(): void;
|
|
784
|
+
onBackdropClick(callback: () => void): void;
|
|
785
|
+
// Only the topmost open overlay is told, so a dropdown inside a dialog takes Escape
|
|
786
|
+
// for itself and leaves the dialog open.
|
|
787
|
+
onKeydown(callback: (event: KeyboardEvent) => void): void;
|
|
688
788
|
}
|
|
689
789
|
|
|
690
790
|
class OverlayPosition {
|
|
691
|
-
flexibleConnectedTo(
|
|
791
|
+
flexibleConnectedTo(origin: ElementRef | HTMLElement): this;
|
|
692
792
|
withPositions(positions: ConnectionPosition[]): this;
|
|
793
|
+
// `start` / `end` are logical and read from the origin element; this overrides that.
|
|
794
|
+
withDirection(direction: 'ltr' | 'rtl' | null): this;
|
|
693
795
|
}
|
|
694
796
|
```
|
|
695
797
|
|
|
798
|
+
`HUB_DROPDOWN_POSITIONS` is the ready-made fallback chain for a dropdown — below the
|
|
799
|
+
origin, flipping above when there is no room — expressed logically so one list serves
|
|
800
|
+
both text directions:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
import { HUB_DROPDOWN_POSITIONS } from 'ng-hub-ui-utils';
|
|
804
|
+
|
|
805
|
+
overlayService.position().flexibleConnectedTo(origin).withPositions([...HUB_DROPDOWN_POSITIONS]);
|
|
806
|
+
```
|
|
807
|
+
|
|
696
808
|
#### ScrollBar Service
|
|
697
809
|
|
|
698
810
|
```typescript
|
|
@@ -702,14 +814,26 @@ class ScrollBar {
|
|
|
702
814
|
}
|
|
703
815
|
```
|
|
704
816
|
|
|
705
|
-
#### PopupService
|
|
817
|
+
#### PopupService<T>
|
|
818
|
+
|
|
819
|
+
A concrete generic class, not an abstract one: it takes the popup component type in its
|
|
820
|
+
constructor, and it reads its collaborators with `inject()`, so it has to be created inside
|
|
821
|
+
an injection context — as an `@Injectable()` subclass, or from a factory provider.
|
|
706
822
|
|
|
707
823
|
```typescript
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
824
|
+
class PopupService<T> {
|
|
825
|
+
constructor(componentType: Type<T>);
|
|
826
|
+
open(
|
|
827
|
+
content?: string | TemplateRef<any>,
|
|
828
|
+
templateContext?: any,
|
|
829
|
+
animation?: boolean
|
|
830
|
+
): { windowRef: ComponentRef<T>; transition$: Observable<void> };
|
|
831
|
+
close(animation?: boolean): Observable<void>;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// The nodes and view a popup projects, returned internally by the content resolver.
|
|
835
|
+
class ContentRef {
|
|
836
|
+
constructor(nodes: Node[][], viewRef?: ViewRef, componentRef?: ComponentRef<any>);
|
|
713
837
|
}
|
|
714
838
|
```
|
|
715
839
|
|
|
@@ -729,7 +853,7 @@ This library doesn't include visual components, but support utilities used by ot
|
|
|
729
853
|
| Overlay Service | Flexible overlay positioning system | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
730
854
|
| Focus Trap | Focus management in modals/overlays | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
731
855
|
| Scrollbar | Scrollbar compensation | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
732
|
-
| Popup Service |
|
|
856
|
+
| Popup Service | Host for dynamically created popups | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
733
857
|
| Transitions | Smooth animations | ng-hub-ui-accordion, ng-hub-ui-modal |
|
|
734
858
|
| Type Guards | Type validation functions | ng-hub-ui-stepper |
|
|
735
859
|
| Pipes | Template utilities | All Hub UI components |
|
|
@@ -2683,6 +2683,18 @@ const TOOLTIP_THEME_VARS = [
|
|
|
2683
2683
|
'--hub-tooltip-white-space',
|
|
2684
2684
|
'--hub-tooltip-text-align'
|
|
2685
2685
|
];
|
|
2686
|
+
/**
|
|
2687
|
+
* Grace period, in milliseconds, between the pointer leaving the host and the label
|
|
2688
|
+
* starting to fade.
|
|
2689
|
+
*
|
|
2690
|
+
* WCAG 1.4.13 asks hover-revealed content to stay put while the pointer travels onto it,
|
|
2691
|
+
* and `offset` opens exactly the gap that trip has to cross. Without the grace period the
|
|
2692
|
+
* bubble is already fading before the pointer can reach it, so a label longer than its box
|
|
2693
|
+
* could be read only for as long as it takes to cross 8px.
|
|
2694
|
+
*/
|
|
2695
|
+
const HOVER_GRACE_MS = 100;
|
|
2696
|
+
/** Feeds the unique `id` each bubble needs so `aria-describedby` can point at it. */
|
|
2697
|
+
let nextTooltipId = 0;
|
|
2686
2698
|
/**
|
|
2687
2699
|
* Framework-agnostic tooltip engine.
|
|
2688
2700
|
*
|
|
@@ -2692,6 +2704,10 @@ const TOOLTIP_THEME_VARS = [
|
|
|
2692
2704
|
* (e.g. a badge overflow tooltip) that want the exact same visual contract
|
|
2693
2705
|
* without re-implementing the DOM logic.
|
|
2694
2706
|
*
|
|
2707
|
+
* The bubble is a `role="tooltip"` element the host is `aria-describedby` while it is on
|
|
2708
|
+
* screen, so the label exists for a screen reader and not only for a pointer, and it obeys
|
|
2709
|
+
* WCAG 1.4.13: Escape dismisses it, and it survives the trip of the pointer onto it.
|
|
2710
|
+
*
|
|
2695
2711
|
* Styles ship in `styles/tooltip.scss`. Import once in your app:
|
|
2696
2712
|
* `@use 'ng-hub-ui-utils/styles/tooltip';`.
|
|
2697
2713
|
*/
|
|
@@ -2699,14 +2715,30 @@ class HubTooltipController {
|
|
|
2699
2715
|
host;
|
|
2700
2716
|
tooltipEl = null;
|
|
2701
2717
|
hideTimeout = null;
|
|
2718
|
+
leaveTimeout = null;
|
|
2702
2719
|
text = '';
|
|
2703
2720
|
placement = 'top';
|
|
2704
2721
|
delay = 150;
|
|
2705
2722
|
offset = 8;
|
|
2706
2723
|
doc;
|
|
2707
2724
|
view;
|
|
2725
|
+
/** Id of this controller's bubble, minted once so the host can be described by it. */
|
|
2726
|
+
tooltipId = `hub-tooltip-${++nextTooltipId}`;
|
|
2708
2727
|
onShow = () => this.show();
|
|
2709
2728
|
onHide = () => this.hide();
|
|
2729
|
+
onPointerLeave = () => this.scheduleHide();
|
|
2730
|
+
onTooltipEnter = () => this.retain();
|
|
2731
|
+
/**
|
|
2732
|
+
* Escape dismisses the label without moving the pointer or the focus, which is the
|
|
2733
|
+
* half of WCAG 1.4.13 a hover-only bubble cannot satisfy on its own. Listened for on
|
|
2734
|
+
* the document, and in the capture phase, so it still reaches us on a page whose own
|
|
2735
|
+
* handlers stop the event before it bubbles.
|
|
2736
|
+
*/
|
|
2737
|
+
onKeydown = (event) => {
|
|
2738
|
+
if (event.key === 'Escape') {
|
|
2739
|
+
this.hide();
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2710
2742
|
/**
|
|
2711
2743
|
* @param host Element the tooltip is anchored to and whose pointer/focus
|
|
2712
2744
|
* events trigger the tooltip.
|
|
@@ -2719,7 +2751,7 @@ class HubTooltipController {
|
|
|
2719
2751
|
this.setOptions(options);
|
|
2720
2752
|
this.host.addEventListener('mouseenter', this.onShow);
|
|
2721
2753
|
this.host.addEventListener('focus', this.onShow);
|
|
2722
|
-
this.host.addEventListener('mouseleave', this.
|
|
2754
|
+
this.host.addEventListener('mouseleave', this.onPointerLeave);
|
|
2723
2755
|
this.host.addEventListener('blur', this.onHide);
|
|
2724
2756
|
this.host.addEventListener('click', this.onHide);
|
|
2725
2757
|
}
|
|
@@ -2761,20 +2793,29 @@ class HubTooltipController {
|
|
|
2761
2793
|
destroy() {
|
|
2762
2794
|
this.host.removeEventListener('mouseenter', this.onShow);
|
|
2763
2795
|
this.host.removeEventListener('focus', this.onShow);
|
|
2764
|
-
this.host.removeEventListener('mouseleave', this.
|
|
2796
|
+
this.host.removeEventListener('mouseleave', this.onPointerLeave);
|
|
2765
2797
|
this.host.removeEventListener('blur', this.onHide);
|
|
2766
2798
|
this.host.removeEventListener('click', this.onHide);
|
|
2767
2799
|
this.removeElement();
|
|
2768
2800
|
}
|
|
2769
2801
|
/** Creates, positions and reveals the tooltip element. */
|
|
2770
2802
|
show() {
|
|
2771
|
-
if (
|
|
2803
|
+
if (!this.text) {
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
// A bubble that is still fading out is brought back rather than left to expire:
|
|
2807
|
+
// the pointer returning to the host is the user asking for the label again.
|
|
2808
|
+
if (this.tooltipEl) {
|
|
2809
|
+
this.retain();
|
|
2772
2810
|
return;
|
|
2773
2811
|
}
|
|
2774
2812
|
this.clearHideTimeout();
|
|
2775
2813
|
const el = this.doc.createElement('span');
|
|
2776
2814
|
el.textContent = this.text;
|
|
2777
2815
|
el.classList.add('hub-tooltip', `hub-tooltip--${this.placement}`);
|
|
2816
|
+
// The bubble is the host's description, and it has to be findable by id to say so.
|
|
2817
|
+
el.id = this.tooltipId;
|
|
2818
|
+
el.setAttribute('role', 'tooltip');
|
|
2778
2819
|
// Taken out of flow here rather than left to the stylesheet alone.
|
|
2779
2820
|
//
|
|
2780
2821
|
// The sheet ships `position: absolute` and this is the same value, so nothing changes
|
|
@@ -2791,28 +2832,87 @@ class HubTooltipController {
|
|
|
2791
2832
|
el.style.position = 'absolute';
|
|
2792
2833
|
el.style.transitionDuration = `${this.delay}ms`;
|
|
2793
2834
|
this.forwardThemeVars(el);
|
|
2835
|
+
el.addEventListener('mouseenter', this.onTooltipEnter);
|
|
2836
|
+
el.addEventListener('mouseleave', this.onHide);
|
|
2794
2837
|
this.doc.body.appendChild(el);
|
|
2795
2838
|
this.tooltipEl = el;
|
|
2839
|
+
this.describeHost();
|
|
2840
|
+
this.doc.addEventListener('keydown', this.onKeydown, true);
|
|
2796
2841
|
this.position();
|
|
2797
2842
|
el.classList.add('hub-tooltip--show');
|
|
2798
2843
|
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Fades the tooltip out after the grace period, so the pointer can cross the gap the
|
|
2846
|
+
* offset opens between host and bubble without the label vanishing on the way.
|
|
2847
|
+
*/
|
|
2848
|
+
scheduleHide() {
|
|
2849
|
+
if (!this.tooltipEl) {
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
this.clearLeaveTimeout();
|
|
2853
|
+
this.leaveTimeout = setTimeout(() => this.hide(), HOVER_GRACE_MS);
|
|
2854
|
+
}
|
|
2855
|
+
/** Cancels a pending hide and brings a fading bubble back to full opacity. */
|
|
2856
|
+
retain() {
|
|
2857
|
+
this.clearLeaveTimeout();
|
|
2858
|
+
this.clearHideTimeout();
|
|
2859
|
+
if (this.tooltipEl) {
|
|
2860
|
+
this.tooltipEl.style.pointerEvents = '';
|
|
2861
|
+
this.tooltipEl.classList.add('hub-tooltip--show');
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2799
2864
|
/** Fades the tooltip out and removes it after the fade completes. */
|
|
2800
2865
|
hide() {
|
|
2866
|
+
this.clearLeaveTimeout();
|
|
2801
2867
|
if (!this.tooltipEl) {
|
|
2802
2868
|
return;
|
|
2803
2869
|
}
|
|
2804
2870
|
this.tooltipEl.classList.remove('hub-tooltip--show');
|
|
2871
|
+
// Fading, so the pointer is not coming: stop the bubble from catching clicks meant
|
|
2872
|
+
// for whatever it floats over while it is invisible but still in the document.
|
|
2873
|
+
this.tooltipEl.style.pointerEvents = 'none';
|
|
2805
2874
|
this.clearHideTimeout();
|
|
2806
2875
|
this.hideTimeout = setTimeout(() => this.removeElement(), this.delay);
|
|
2807
2876
|
}
|
|
2808
2877
|
/** Removes the tooltip element immediately. */
|
|
2809
2878
|
removeElement() {
|
|
2810
2879
|
this.clearHideTimeout();
|
|
2880
|
+
this.clearLeaveTimeout();
|
|
2811
2881
|
if (this.tooltipEl) {
|
|
2882
|
+
this.doc.removeEventListener('keydown', this.onKeydown, true);
|
|
2883
|
+
this.tooltipEl.removeEventListener('mouseenter', this.onTooltipEnter);
|
|
2884
|
+
this.tooltipEl.removeEventListener('mouseleave', this.onHide);
|
|
2885
|
+
this.undescribeHost();
|
|
2812
2886
|
this.tooltipEl.remove();
|
|
2813
2887
|
this.tooltipEl = null;
|
|
2814
2888
|
}
|
|
2815
2889
|
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Points the host at the live bubble so assistive technology reads the label as the
|
|
2892
|
+
* host's description. Any `aria-describedby` the consumer already wrote is kept: the
|
|
2893
|
+
* tooltip joins that list instead of replacing it, and leaves it as it found it.
|
|
2894
|
+
*/
|
|
2895
|
+
describeHost() {
|
|
2896
|
+
const tokens = this.describedBy();
|
|
2897
|
+
if (!tokens.includes(this.tooltipId)) {
|
|
2898
|
+
tokens.push(this.tooltipId);
|
|
2899
|
+
}
|
|
2900
|
+
this.host.setAttribute('aria-describedby', tokens.join(' '));
|
|
2901
|
+
}
|
|
2902
|
+
/** Removes this tooltip from the host's description, dropping an emptied attribute. */
|
|
2903
|
+
undescribeHost() {
|
|
2904
|
+
const tokens = this.describedBy().filter((id) => id !== this.tooltipId);
|
|
2905
|
+
if (tokens.length) {
|
|
2906
|
+
this.host.setAttribute('aria-describedby', tokens.join(' '));
|
|
2907
|
+
}
|
|
2908
|
+
else {
|
|
2909
|
+
this.host.removeAttribute('aria-describedby');
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
/** Current `aria-describedby` of the host, as a token list. */
|
|
2913
|
+
describedBy() {
|
|
2914
|
+
return (this.host.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
|
|
2915
|
+
}
|
|
2816
2916
|
/**
|
|
2817
2917
|
* Copies any `--hub-tooltip-*` value defined on the host (or its scope) onto
|
|
2818
2918
|
* the body-portaled tooltip, so scoped theming applies despite the portal.
|
|
@@ -2835,6 +2935,12 @@ class HubTooltipController {
|
|
|
2835
2935
|
this.hideTimeout = null;
|
|
2836
2936
|
}
|
|
2837
2937
|
}
|
|
2938
|
+
clearLeaveTimeout() {
|
|
2939
|
+
if (this.leaveTimeout !== null) {
|
|
2940
|
+
clearTimeout(this.leaveTimeout);
|
|
2941
|
+
this.leaveTimeout = null;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2838
2944
|
/** Positions the tooltip around the host according to the current placement. */
|
|
2839
2945
|
position() {
|
|
2840
2946
|
if (!this.tooltipEl) {
|