Skip to content

Commit 9b93f1d

Browse files
committed
Refactor tooltip implementation across components
- Updated `kol-link` component to use a new tooltip structure. - Modified `kol-popover-button` tests to locate the tooltip using the new class. - Replaced `KolTooltipWcTag` with `TooltipFC` in `kol-table-stateless` component. - Enhanced character limit tests for input components to improve reliability. - Integrated `TooltipFC` into `FieldControl` and `FormField` components, replacing the deprecated `FormFieldTooltipFc`. - Removed `FormFieldTooltip` component and its associated tests and snapshots. - Introduced a new `TooltipController` to manage tooltip behavior and positioning. - Added `id` prop definition for better tooltip management. - Updated various internal controllers to align with the new base controller structure. - Updated theme snapshots to reflect visual changes.
1 parent 8229748 commit 9b93f1d

File tree

36 files changed

+599
-263
lines changed

36 files changed

+599
-263
lines changed

.claude/commands/migrate-to-skeleton.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public componentWillLoad(): void {
166166
```typescript
167167
export class MyController extends BaseController<MyApi> implements ControllerInterface<MyApi> {
168168
public constructor(setState: SetStateFn<MyApi>, getState: GetStateFn<MyApi>) {
169-
super(myPropsConfig, setState, getState);
169+
super(setState, getState, myPropsConfig);
170170
}
171171

172172
public watchName(value?: string): void {

docs/tutorials/NEW_COMPONENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ import { myComponentPropsConfig } from './api';
9898

9999
export class MyComponentController extends BaseController<MyComponentApi> implements ControllerInterface<MyComponentApi> {
100100
public constructor(setState: SetStateFn<MyComponentApi>, getState: GetStateFn<MyComponentApi>) {
101-
super(myComponentPropsConfig, setState, getState);
101+
super(setState, getState, myComponentPropsConfig);
102102
}
103103

104104
public componentWillLoad(props: ResolvedInputProps<MyComponentApi>): void {

packages/adapters/hydrate/test/__snapshots__/components.spec.js.mocha-snapshot

Lines changed: 6 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@mixin kol-abbr-styles() {
2+
:host > abbr {
3+
cursor: help;
4+
}
5+
}

packages/components/src/components/abbr/shadow.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSX } from '@stencil/core';
22
import { Component, h, Host, Prop, State, Watch } from '@stencil/core';
3-
import { KolTooltipWcTag } from '../../core/component-names';
3+
import { TooltipFC } from '../../internal/functional-components/tooltip/component';
4+
import { TooltipController } from '../../internal/functional-components/tooltip/controller';
45
import type { AbbrAPI, AbbrStates, LabelPropType } from '../../schema';
56
import { validateLabel } from '../../schema';
67

@@ -18,14 +19,31 @@ import { validateLabel } from '../../schema';
1819
shadow: true,
1920
})
2021
export class KolAbbr implements AbbrAPI {
22+
private abbrRef?: HTMLElement;
23+
24+
private readonly tooltipCtrl = new TooltipController();
25+
26+
private readonly setAbbrRef = (ref?: HTMLElement) => {
27+
this.abbrRef = ref;
28+
};
29+
2130
public render(): JSX.Element {
2231
return (
2332
<Host class="kol-abbr">
2433
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
25-
<abbr tabIndex={this.state._label ? 0 : undefined}>
34+
<abbr ref={this.setAbbrRef} tabIndex={this.state._label ? 0 : undefined}>
2635
<slot />
2736
</abbr>
28-
{this.state._label ? <KolTooltipWcTag aria-hidden="true" _label={this.state._label}></KolTooltipWcTag> : null}
37+
{this.state._label ? (
38+
<TooltipFC
39+
aria-hidden="true"
40+
label={typeof this.state._label === 'string' ? this.state._label : ''}
41+
badgeText={''}
42+
id={this.tooltipCtrl.getRenderProp('id')}
43+
refFloating={this.tooltipCtrl.setTooltipElementRef}
44+
refArrow={this.tooltipCtrl.setArrowElementRef}
45+
/>
46+
) : null}
2947
</Host>
3048
);
3149
}
@@ -42,9 +60,23 @@ export class KolAbbr implements AbbrAPI {
4260
@Watch('_label')
4361
public validateLabel(value?: LabelPropType): void {
4462
validateLabel(this, value);
63+
this.tooltipCtrl.watchLabel(value);
4564
}
4665

4766
public componentWillLoad(): void {
4867
this.validateLabel(this._label);
68+
this.tooltipCtrl.componentWillLoad({
69+
label: typeof this._label === 'string' ? this._label : '',
70+
});
71+
}
72+
73+
public componentDidRender(): void {
74+
if (this.abbrRef) {
75+
this.tooltipCtrl.syncListeners(undefined, this.abbrRef, true);
76+
}
77+
}
78+
79+
public disconnectedCallback(): void {
80+
this.tooltipCtrl.destroy();
4981
}
5082
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
@use '../@shared/global' as *;
2+
@use '../@shared/abbr.mixin' as *;
23
@use '../tooltip/style.scss' as *;
4+
5+
@layer kol-component {
6+
@include kol-abbr-styles();
7+
}

packages/components/src/components/abbr/test/__snapshots__/snapshot.spec.tsx.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ exports[`kol-abbr should render with _label="Text" 1`] = `
66
<abbr tabindex="0">
77
<slot></slot>
88
</abbr>
9-
<kol-tooltip-wc _label="Text" aria-hidden="true"></kol-tooltip-wc>
9+
<div class="kol-tooltip__floating">
10+
<div class="kol-tooltip__arrow"></div>
11+
<span class="kol-span kol-tooltip__content">
12+
<span class="kol-span__container">
13+
<span class="kol-span__label">
14+
Text
15+
</span>
16+
<span aria-hidden="true" class="kol-span__label" hidden=""></span>
17+
</span>
18+
</span>
19+
</div>
1020
</template>
1121
</kol-abbr>
1222
`;

packages/components/src/components/button/component.tsx

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ import {
5151
} from '../../schema';
5252
import { validateTabIndex } from '../../schema/props/tab-index';
5353

54-
import { KolTooltipWcTag } from '../../core/component-names';
5554
import { SpanFC } from '../../internal/functional-components/span/component';
55+
import { TooltipFC } from '../../internal/functional-components/tooltip/component';
56+
import { TooltipController } from '../../internal/functional-components/tooltip/controller';
5657
import type { AriaHasPopupPropType } from '../../schema/props/aria-has-popup';
5758
import { validateAccessAndShortKey } from '../../schema/validators/access-and-short-key';
5859
import clsx from '../../utils/clsx';
@@ -72,7 +73,15 @@ import { AssociatedInputController } from '../input-adapter-leanup/associated.co
7273
export class KolButtonWc implements ButtonAPI, FocusableElement {
7374
@Element() private readonly host?: HTMLKolButtonWcElement;
7475
private buttonRef?: HTMLButtonElement;
75-
private tooltipRef?: HTMLKolTooltipWcElement;
76+
private readonly tooltipCtrl = new TooltipController(
77+
() => {
78+
// No-op: Tooltip state is managed internally by the controller
79+
},
80+
() => {
81+
// No-op: Tooltip state is managed internally by the controller
82+
return undefined as never;
83+
},
84+
);
7685

7786
/**
7887
* Sets focus on the internal element.
@@ -94,21 +103,9 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
94103
this.buttonRef = ref;
95104
};
96105

97-
private readonly setTooltipRef = (ref?: HTMLKolTooltipWcElement) => {
98-
this.tooltipRef = ref;
99-
};
100-
101-
private readonly hideTooltip = () => {
102-
void this.tooltipRef?.hideTooltip();
103-
};
104-
105106
private readonly onClick = (event: MouseEvent) => {
106107
event.stopPropagation();
107108

108-
if (this.state._hideLabel) {
109-
void this.hideTooltip();
110-
}
111-
112109
if (this.state._type === 'submit') {
113110
propagateSubmitEventToForm({
114111
form: this.host,
@@ -182,20 +179,16 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
182179
<slot name="expert" slot="expert"></slot>
183180
</SpanFC>
184181
</button>
185-
{hideLabel && (
186-
<KolTooltipWcTag
187-
ref={this.setTooltipRef}
188-
/**
189-
* Dieses Aria-Hidden verhindert das doppelte Vorlesen des Labels,
190-
* verhindert aber nicht das Aria-Labelledby vorgelesen wird.
191-
*/
192-
aria-hidden="true"
193-
hidden={hasExpertSlot}
194-
class="kol-button__tooltip"
195-
_badgeText={badgeText}
196-
_align={this.state._tooltipAlign}
197-
_label={typeof this.state._label === 'string' ? this.state._label : ''}
198-
></KolTooltipWcTag>
182+
{hideLabel && !hasExpertSlot && (
183+
<div class="kol-button__tooltip">
184+
<TooltipFC
185+
badgeText={badgeText || ''}
186+
label={typeof this.state._label === 'string' ? this.state._label : ''}
187+
id={this.tooltipCtrl.getRenderProp('id')}
188+
refFloating={this.tooltipCtrl.setTooltipElementRef}
189+
refArrow={this.tooltipCtrl.setArrowElementRef}
190+
/>
191+
</div>
199192
)}
200193
</Host>
201194
);
@@ -399,6 +392,7 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
399392
validateLabelWithExpertSlot(this, value, {
400393
required: true,
401394
});
395+
this.tooltipCtrl.watchLabel(typeof value === 'string' ? value : undefined);
402396
}
403397

404398
@Watch('_name')
@@ -435,6 +429,7 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
435429
@Watch('_tooltipAlign')
436430
public validateTooltipAlign(value?: TooltipAlignPropType): void {
437431
validateTooltipAlign(this, value);
432+
this.tooltipCtrl.watchAlign(value);
438433
}
439434

440435
@Watch('_type')
@@ -477,5 +472,19 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
477472
this.validateValue(this._value);
478473
this.validateVariant(this._variant);
479474
validateAccessAndShortKey(this._accessKey, this._shortKey);
475+
this.tooltipCtrl.componentWillLoad({
476+
label: typeof this.state._label === 'string' ? this.state._label : '',
477+
align: this._tooltipAlign,
478+
});
479+
}
480+
481+
public componentDidRender(): void {
482+
if (this.buttonRef) {
483+
this.tooltipCtrl.syncListeners(undefined, this.buttonRef, true);
484+
}
485+
}
486+
487+
public disconnectedCallback(): void {
488+
this.tooltipCtrl.destroy();
480489
}
481490
}

packages/components/src/components/link/component.tsx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { JSX } from '@stencil/core';
22
import { Component, Element, h, Host, Method, Prop, State, Watch } from '@stencil/core';
3-
import { KolTooltipWcTag } from '../../core/component-names';
43
import { IconFC } from '../../internal/functional-components/icon/component';
4+
import { TooltipFC } from '../../internal/functional-components/tooltip/component';
5+
import { TooltipController } from '../../internal/functional-components/tooltip/controller';
56
import type {
67
AccessKeyPropType,
78
AlternativeButtonLinkRolePropType,
@@ -75,7 +76,15 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
7576
@Element() private readonly host?: HTMLKolLinkElement;
7677

7778
private anchorRef?: HTMLAnchorElement;
78-
private tooltipRef?: HTMLKolTooltipWcElement;
79+
private readonly tooltipCtrl = new TooltipController(
80+
() => {
81+
// No-op: Tooltip state is managed internally by the controller
82+
},
83+
() => {
84+
// No-op: Tooltip state is managed internally by the controller
85+
return undefined as never;
86+
},
87+
);
7988
private unsubscribeOnLocationChange?: UnsubscribeFunction;
8089

8190
private readonly translateOpenLinkInTab = translate('kol-open-link-in-tab');
@@ -84,14 +93,6 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
8493
this.anchorRef = ref;
8594
};
8695

87-
private readonly setTooltipRef = (ref?: HTMLKolTooltipWcElement) => {
88-
this.tooltipRef = ref;
89-
};
90-
91-
private readonly hideTooltip = () => {
92-
void this.tooltipRef?.hideTooltip();
93-
};
94-
9596
/**
9697
* Sets focus on the internal element.
9798
*/
@@ -109,10 +110,6 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
109110
}
110111

111112
private readonly onClick = (event: Event) => {
112-
if (this.state._hideLabel) {
113-
this.hideTooltip();
114-
}
115-
116113
if (this.state._disabled === true) {
117114
event.preventDefault();
118115
} else {
@@ -219,20 +216,16 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
219216
/>
220217
)}
221218
</a>
222-
{this.state._hideLabel === true && (
223-
<KolTooltipWcTag
224-
/**
225-
* Dieses Aria-Hidden verhindert das doppelte Vorlesen des Labels,
226-
* verhindert aber nicht das Aria-Labelledby vorgelesen wird.
227-
*/
228-
aria-hidden="true"
229-
class="kol-link__tooltip"
230-
ref={this.setTooltipRef}
231-
hidden={hasExpertSlot}
232-
_badgeText={this.state._accessKey || this.state._shortKey}
233-
_align={this.state._tooltipAlign}
234-
_label={this.state._label || this.state._href}
235-
></KolTooltipWcTag>
219+
{this.state._hideLabel === true && !hasExpertSlot && (
220+
<div class="kol-link__tooltip">
221+
<TooltipFC
222+
badgeText={this.state._accessKey || this.state._shortKey || ''}
223+
label={typeof this.state._label === 'string' ? this.state._label : typeof this.state._href === 'string' ? this.state._href : ''}
224+
id={this.tooltipCtrl.getRenderProp('id')}
225+
refFloating={this.tooltipCtrl.setTooltipElementRef}
226+
refArrow={this.tooltipCtrl.setArrowElementRef}
227+
/>
228+
</div>
236229
)}
237230
</Host>
238231
);
@@ -426,6 +419,7 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
426419
@Watch('_label')
427420
public validateLabel(value?: LabelWithExpertSlotPropType): void {
428421
validateLabelWithExpertSlot(this, value);
422+
this.tooltipCtrl.watchLabel(typeof value === 'string' ? value : undefined);
429423
}
430424

431425
@Watch('_on')
@@ -457,6 +451,7 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
457451
@Watch('_tooltipAlign')
458452
public validateTooltipAlign(value?: TooltipAlignPropType): void {
459453
validateTooltipAlign(this, value);
454+
this.tooltipCtrl.watchAlign(value);
460455
}
461456

462457
@Watch('_variant')
@@ -490,11 +485,22 @@ export class KolLinkWc implements InternalLinkAPI, FocusableElement {
490485
this.state._ariaCurrent = location === this.state._href ? this.state._ariaCurrentValue : undefined;
491486
});
492487
validateAccessAndShortKey(this._accessKey, this._shortKey);
488+
this.tooltipCtrl.componentWillLoad({
489+
label: typeof this.state._label === 'string' ? this.state._label : typeof this.state._href === 'string' ? this.state._href : '',
490+
align: this._tooltipAlign,
491+
});
492+
}
493+
494+
public componentDidRender(): void {
495+
if (this.anchorRef) {
496+
this.tooltipCtrl.syncListeners(undefined, this.anchorRef, true);
497+
}
493498
}
494499

495500
public disconnectedCallback(): void {
496501
if (this.unsubscribeOnLocationChange) {
497502
this.unsubscribeOnLocationChange();
498503
}
504+
this.tooltipCtrl.destroy();
499505
}
500506
}

packages/components/src/components/link/test/__snapshots__/snapshot.spec.tsx.snap

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,19 @@ exports[`kol-link should render with _href="https://example.com" _icons="codicon
8888
</span>
8989
<i aria-hidden="true" class="kol-icon kol-icon__icon kol-link__icon kolicon-link-external" role="presentation"></i>
9090
</a>
91-
<kol-tooltip-wc _align="top" _label="Label" aria-hidden="true" class="kol-link__tooltip"></kol-tooltip-wc>
91+
<div class="kol-link__tooltip">
92+
<div class="kol-tooltip__floating">
93+
<div class="kol-tooltip__arrow"></div>
94+
<span class="kol-span kol-tooltip__content">
95+
<span class="kol-span__container">
96+
<span class="kol-span__label">
97+
Label
98+
</span>
99+
<span aria-hidden="true" class="kol-span__label" hidden=""></span>
100+
</span>
101+
</span>
102+
</div>
103+
</div>
92104
</kol-link-wc>
93105
</template>
94106
</kol-link>

0 commit comments

Comments
 (0)