Skip to content

Commit 6466f9e

Browse files
committed
feat(tooltip): enhance tooltip animations and add customization options
1 parent 1c25ecc commit 6466f9e

File tree

8 files changed

+130
-74
lines changed

8 files changed

+130
-74
lines changed

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ The samples are located in `packages/samples/react` and demonstrate how to use t
231231
- `.editorconfig` sets `indent_style = tab` and `max_line_length = 160` for code files. Markdown and YAML files use spaces.
232232
- ESLint and Stylelint are run using `pnpm lint`. Pre‑commit hooks run `lint-staged` which formats and lints changed files. Lint rules should **not** be disabled via inline comments. Instead, describe the problem and work towards a clean solution.
233233
- Lists and enumerations in code should be kept in alphanumeric order. This also applies to import specifiers and union type literals.
234+
- Do not disable ESLint, Stylelint or TypeScript rules inline. Fix the code instead of turning such rules off.
235+
- ESLint and Stylelint are run using `pnpm lint`. Pre‑commit hooks run `lint-staged` which formats and lints changed files.
236+
- Lists and enumerations in code should be kept in alphabetical order (see `docs/tutorials/NEW_COMPONENT.md`).
234237
- Commit messages follow the **Conventional Commits** specification.
235238
- See also the [Contributing Guide](CONTRIBUTING.md) for more details on coding conventions and best practices.
236239
- Spell "KoliBri" with this casing in all documentation and code. The only exception is the component named KolKolibri.

packages/components/src/components/abbr/abbr.e2e.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test.describe('kol-abbr', () => {
88
const tooltip = kolAbbr.locator('kol-tooltip-wc kol-span-wc');
99
await expect(tooltip).not.toBeVisible();
1010
await kolAbbr.hover();
11+
await page.waitForChanges();
1112
await expect(tooltip).toBeVisible();
1213
await expect(tooltip).toContainText('for example');
1314
});

packages/components/src/components/tooltip/component.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ export class KolTooltipWc implements TooltipAPI {
4242
if (this.previousSibling && this.tooltipElement /* SSR instanceof HTMLElement */) {
4343
showOverlay(this.tooltipElement);
4444
tooltipOpened();
45+
this.tooltipElement.classList.remove('hide');
46+
this.tooltipElement.classList.add('show');
4547
this.tooltipElement.style.setProperty('display', 'block');
46-
getDocument().addEventListener('keyup', this.hideTooltipByEscape);
48+
getDocument().addEventListener('keyup', this.hideTooltipByEscape, {
49+
once: true,
50+
});
4751

4852
const target = this.previousSibling;
4953
const tooltipEl = this.tooltipElement;
@@ -57,8 +61,9 @@ export class KolTooltipWc implements TooltipAPI {
5761
if (this.tooltipElement /* SSR instanceof HTMLElement */) {
5862
hideOverlay(this.tooltipElement);
5963
tooltipClosed();
60-
this.tooltipElement.style.setProperty('display', 'none');
61-
this.tooltipElement.style.setProperty('visibility', 'hidden');
64+
this.tooltipElement.classList.remove('show');
65+
this.tooltipElement.classList.add('hide');
66+
6267
if (this.cleanupAutoPositioning) {
6368
this.cleanupAutoPositioning();
6469
this.cleanupAutoPositioning = undefined;
@@ -73,35 +78,38 @@ export class KolTooltipWc implements TooltipAPI {
7378
}
7479
};
7580

76-
private handleMouseEnter() {
81+
private handleMouseEnter = (): void => {
7782
this.hasMouseIn = true;
7883
this.showOrHideTooltip();
79-
}
80-
private handleMouseleave() {
81-
this.hasMouseIn = false;
84+
};
85+
86+
private handleMouseleave = (event: Event): void => {
87+
this.hasMouseIn = this.tooltipElement?.contains((event as MouseEvent).relatedTarget as Node) ?? false;
8288
this.showOrHideTooltip();
83-
}
84-
private handleFocusIn() {
89+
};
90+
91+
private handleFocusIn = (): void => {
8592
this.hasFocusIn = true;
8693
this.showOrHideTooltip();
87-
}
88-
private handleFocusout() {
94+
};
95+
96+
private handleFocusout = (): void => {
8997
this.hasFocusIn = false;
9098
this.showOrHideTooltip();
91-
}
99+
};
92100

93101
private addListeners = (el: Element): void => {
94-
el.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
95-
el.addEventListener('mouseleave', this.handleMouseleave.bind(this));
96-
el.addEventListener('focusin', this.handleFocusIn.bind(this));
97-
el.addEventListener('focusout', this.handleFocusout.bind(this));
102+
el.addEventListener('mouseenter', this.handleMouseEnter);
103+
el.addEventListener('mouseleave', this.handleMouseleave);
104+
el.addEventListener('focusin', this.handleFocusIn);
105+
el.addEventListener('focusout', this.handleFocusout);
98106
};
99107

100108
private removeListeners = (el: Element): void => {
101-
el.removeEventListener('mouseenter', this.handleMouseEnter.bind(this));
102-
el.removeEventListener('mouseleave', this.handleMouseleave.bind(this));
103-
el.removeEventListener('focusin', this.handleFocusIn.bind(this));
104-
el.removeEventListener('focusout', this.handleFocusout.bind(this));
109+
el.removeEventListener('mouseenter', this.handleMouseEnter);
110+
el.removeEventListener('mouseleave', this.handleMouseleave);
111+
el.removeEventListener('focusin', this.handleFocusIn);
112+
el.removeEventListener('focusout', this.handleFocusout);
105113
};
106114

107115
private resyncListeners = (last?: Element | null, next?: Element | null, replacePreviousSibling = false): void => {
@@ -208,12 +216,13 @@ export class KolTooltipWc implements TooltipAPI {
208216
}
209217

210218
private handleEventListeners(): void {
211-
this.resyncListeners(this.previousSibling, this.host?.previousElementSibling, true);
219+
const nextSibling = this.host?.previousElementSibling ?? null;
220+
this.resyncListeners(this.previousSibling, nextSibling as Element, true);
212221
this.resyncListeners(this.tooltipElement, this.tooltipElement);
213222
}
214223

215224
public connectedCallback(): void {
216-
this.previousSibling = this.host?.previousElementSibling;
225+
this.previousSibling = this.host?.previousElementSibling ?? null;
217226
}
218227

219228
public componentDidRender(): void {

packages/components/src/components/tooltip/readme.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ Um die Breite eines Tooltips zu konfigurieren, kann auf dem umgebenden Container
2929
}
3030
```
3131

32+
## Animation
33+
34+
Die Dauer der Ein- und Ausblendanimation kann \u00fcber CSS-Custom-Properties angepasst werden.
35+
Die Dauer der Ein- und Ausblendanimation wird über `--kolibri-tooltip-animation-duration` gesteuert.
36+
37+
```css
38+
body {
39+
--kolibri-tooltip-animation-duration: 300ms;
40+
}
41+
```
42+
3243
## Links und Referenzen
3344

3445
- <kol-link _href="https://tollwerk.de/projekte/tipps-techniken-inklusiv-barrierefrei/titel-tooltips-toggletips" _target="_blank"></kol-link>

packages/components/src/components/tooltip/style.scss

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,34 @@
55
display: contents;
66
}
77

8-
.kol-tooltip-wc .tooltip-floating {
9-
display: none;
10-
position: fixed;
11-
/* Avoid layout interference - see https://floating-ui.com/docs/computePosition */
12-
top: 0;
13-
left: 0;
14-
/* Can be used to specify the tooltip-width from the outside. Unset by default. */
15-
width: var(--kol-tooltip-width);
16-
max-width: 90vw;
17-
max-height: 90vh;
18-
19-
visibility: hidden;
20-
animation-duration: 0.25s;
21-
animation-iteration-count: 1;
22-
animation-name: fadeInOpacity;
23-
animation-timing-function: ease-in;
8+
.kol-tooltip-wc {
9+
.tooltip-floating {
10+
opacity: 0;
11+
display: none;
12+
position: fixed;
13+
14+
/* Avoid layout interference - see https://floating-ui.com/docs/computePosition */
15+
top: 0;
16+
left: 0;
17+
/* Can be used to specify the tooltip-width from the outside. Unset by default. */
18+
width: var(--kol-tooltip-width, unset);
19+
max-width: 90vw;
20+
max-height: 90vh;
21+
animation-direction: normal;
22+
/* Can be used to specify the animation duration from the outside. 250ms by default. */
23+
animation-duration: var(--kolibri-tooltip-animation-duration, 250ms);
24+
animation-fill-mode: forwards;
25+
animation-iteration-count: 1;
26+
animation-timing-function: ease-in;
27+
28+
&.hide {
29+
animation-name: hideTooltip;
30+
}
31+
32+
&.show {
33+
animation-name: showTooltip;
34+
}
35+
}
2436
}
2537

2638
/* Shared between content and arrow */
@@ -42,7 +54,18 @@
4254
z-index: 1000;
4355
}
4456

45-
@keyframes fadeInOpacity {
57+
@keyframes hideTooltip {
58+
0% {
59+
opacity: 1;
60+
}
61+
62+
100% {
63+
opacity: 0;
64+
display: none;
65+
}
66+
}
67+
68+
@keyframes showTooltip {
4669
0% {
4770
opacity: 0;
4871
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { FC } from 'react';
2+
import React from 'react';
3+
4+
import { KolButton } from '@public-ui/react-v19';
5+
import { SampleDescription } from '../components/SampleDescription';
6+
import { useToasterService } from '../hooks/useToasterService';
7+
8+
export const CustomTooltipCssProperties: FC = () => {
9+
const { dummyClickEventHandler } = useToasterService();
10+
11+
const dummyEventHandler = {
12+
onClick: dummyClickEventHandler,
13+
};
14+
15+
return (
16+
<>
17+
<SampleDescription>
18+
<p>
19+
This sample demonstrates how tooltip animation duration and width can be customized via
20+
<code>--kolibri-tooltip-animation-duration</code> and <code>--kol-tooltip-width</code>.
21+
</p>
22+
</SampleDescription>
23+
24+
<div className="flex justify-center items-center gap-4">
25+
<KolButton
26+
_label="Custom duration"
27+
_hideLabel
28+
style={{ '--kolibri-tooltip-animation-duration': '2500ms' }}
29+
_icons="codicon codicon-clock"
30+
_on={dummyEventHandler}
31+
></KolButton>
32+
<KolButton
33+
_label="Custom width"
34+
_hideLabel
35+
style={{ '--kol-tooltip-width': '400px' }}
36+
_icons="codicon codicon-arrow-both"
37+
_on={dummyEventHandler}
38+
></KolButton>
39+
</div>
40+
</>
41+
);
42+
};

packages/samples/react/src/scenarios/custom-tooltip-width.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.

packages/samples/react/src/scenarios/routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Routes } from '../shares/types';
22
import { AppointmentForm } from './appointment-form/AppointmentForm';
33
import { ChangeTabindex } from './change-tabindex';
4-
import { CustomTooltipWidth } from './custom-tooltip-width';
4+
import { CustomTooltipCssProperties } from './custom-tooltip-css-properties';
55
import { DisabledInteractiveElements } from './disabled-interactive-elements';
66
import { FocusElements } from './focus-elements';
77
import { InputGroupWithError } from './input-group-with-error';
@@ -16,7 +16,7 @@ export const SCENARIO_ROUTES: Routes = {
1616
scenarios: {
1717
'appointment-form': AppointmentForm,
1818
'change-tabindex': ChangeTabindex,
19-
'custom-tooltip-width': CustomTooltipWidth,
19+
'custom-tooltip-css-properties': CustomTooltipCssProperties,
2020
'disabled-interactive-scenario': DisabledInteractiveElements,
2121
'focus-elements': FocusElements,
2222
'input-group-with-error': InputGroupWithError,

0 commit comments

Comments
 (0)