Skip to content

Commit b2b6dc4

Browse files
committed
feat: Refactor focus handling across components to use propagateFocus utility
- Introduced a new utility function `propagateFocus` to manage focus behavior across various components. - Updated focus methods in components such as KolInputFile, KolInputNumber, KolInputPassword, KolInputRadio, KolInputRange, KolInputText, KolLinkButton, KolLink, KolPopoverButton, KolSelect, KolSingleSelect, KolSkipNav, KolSplitButton, KolTabs, KolTextarea, KolToolbar, KolTree, and KolTreeItem to utilize the new utility for improved focus management. - Enhanced focus handling in KolPopover and KolTableStateless components to align with the new focus strategy. - Added visual test routes for new components and scenarios including KolBadge, KolPopoverButton, KolSkipNav, KolSplitButton, KolTableStateless, KolTabs, and KolToolbar.
1 parent d11d839 commit b2b6dc4

File tree

44 files changed

+335
-227
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+335
-227
lines changed

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

Lines changed: 0 additions & 28 deletions
Large diffs are not rendered by default.

packages/components/src/components/_skeleton/ARC42.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ Both are functionally equivalent. Pattern A is more concise; Pattern B allows ad
203203
Arrow function properties automatically bind `this` at definition time. **Never** create new bound instances with `.bind(this)` in DOM event registration, as this causes listener accumulation and memory leaks:
204204

205205
**Memory Leak — DO NOT DO THIS:**
206+
206207
```tsx
207208
private handleToggle(event: Event) { /* ... */ }
208209

@@ -217,6 +218,7 @@ disconnectedCallback() {
217218
```
218219

219220
**Correct — Arrow Function Property (already bound):**
221+
220222
```tsx
221223
private handleToggle = (event: Event) => { /* ... */ } // Arrow property — this is bound here
222224

@@ -246,6 +248,7 @@ private catchElement = (element?: HTMLElement): void => {
246248
```
247249

248250
**Why this matters:**
251+
249252
- `.bind(this)` creates a new function on each call — `addEventListener` and `removeEventListener` must receive the **exact same function reference** to match
250253
- When references don't match, `removeEventListener` silently fails
251254
- Listeners accumulate over time, slowing down event handling and creating garbage collection barriers

packages/components/src/components/accordion/shadow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export class KolAccordion implements AccordionAPI, FocusableElement {
5252
* Sets focus on the internal element.
5353
*/
5454
@Method()
55-
public async focus() {
56-
return Promise.resolve(this.buttonWcRef?.focus());
55+
public async focus(): Promise<void> {
56+
await this.buttonWcRef?.focus(this.host as HTMLElement);
5757
}
5858

5959
private handleOnClick = (event: MouseEvent) => {

packages/components/src/components/badge/shadow.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, h, Method, Prop, State, Watch } from '@stencil/core';
1+
import { Component, Element, h, Method, Prop, State, Watch } from '@stencil/core';
22
import { SpanFC } from '../../internal/functional-components/span/component';
33
import type { BadgeAPI, BadgeStates, FocusableElement, InternalButtonProps, KoliBriIconsProp, LabelPropType, PropColor, Stringified } from '../../schema';
44
import { featureHint, handleColorChange, objectObjectHandler, parseJson, setState, validateColor, validateIcons } from '../../schema';
@@ -22,6 +22,7 @@ featureHint(`[KolBadge] Optimierung des _color-Properties (rgba, rgb, hex usw.).
2222
shadow: true,
2323
})
2424
export class KolBadge implements BadgeAPI, FocusableElement {
25+
@Element() private readonly host!: HTMLKolBadgeElement;
2526
private bgColorStr = '#000';
2627
private colorStr = '#fff';
2728
private readonly id = nonce();
@@ -56,7 +57,7 @@ export class KolBadge implements BadgeAPI, FocusableElement {
5657
*/
5758
@Method()
5859
public async focus(): Promise<void> {
59-
return Promise.resolve(this.smartButtonRef?.focus());
60+
return this.smartButtonRef?.focus(this.host);
6061
}
6162

6263
public render(): JSX.Element {

packages/components/src/components/button-link/shadow.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSX } from '@stencil/core';
2-
import { Component, h, Method, Prop } from '@stencil/core';
2+
import { Component, Element, h, Method, Prop } from '@stencil/core';
33
import { KolButtonWcTag } from '../../core/component-names';
44
import type {
55
AccessKeyPropType,
@@ -41,6 +41,7 @@ import type {
4141
shadow: true,
4242
})
4343
export class KolButtonLink implements ButtonLinkProps, FocusableElement {
44+
@Element() private readonly host?: HTMLKolButtonLinkElement;
4445
private buttonWcRef?: HTMLKolButtonWcElement;
4546

4647
private readonly catchRef = (ref?: HTMLKolButtonWcElement) => {
@@ -60,8 +61,8 @@ export class KolButtonLink implements ButtonLinkProps, FocusableElement {
6061
* Sets focus on the internal element.
6162
*/
6263
@Method()
63-
public async focus() {
64-
return Promise.resolve(this.buttonWcRef?.focus());
64+
public async focus(): Promise<void> {
65+
await this.buttonWcRef?.focus(this.host as HTMLElement);
6566
}
6667

6768
public render(): JSX.Element {

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
ButtonTypePropType,
1313
CustomClassPropType,
1414
DisabledPropType,
15-
FocusableElement,
1615
HideLabelPropType,
1716
IconsPropType,
1817
IdPropType,
@@ -56,6 +55,7 @@ import { SpanFC } from '../../internal/functional-components/span/component';
5655
import type { AriaHasPopupPropType } from '../../schema/props/aria-has-popup';
5756
import { validateAccessAndShortKey } from '../../schema/validators/access-and-short-key';
5857
import clsx from '../../utils/clsx';
58+
import { propagateFocus } from '../../utils/element-focus';
5959
import { dispatchDomEvent, KolEvent } from '../../utils/events';
6060
import { propagateResetEventToForm, propagateSubmitEventToForm } from '../form/controller';
6161
import { AssociatedInputController } from '../input-adapter-leanup/associated.controller';
@@ -67,7 +67,7 @@ import { AssociatedInputController } from '../input-adapter-leanup/associated.co
6767
tag: 'kol-button-wc',
6868
shadow: false,
6969
})
70-
export class KolButtonWc implements ButtonAPI, FocusableElement {
70+
export class KolButtonWc implements ButtonAPI {
7171
@Element() private readonly host?: HTMLKolButtonWcElement;
7272
private buttonRef?: HTMLButtonElement;
7373
private tooltipRef?: HTMLKolTooltipWcElement;
@@ -76,13 +76,8 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
7676
* Sets focus on the internal element.
7777
*/
7878
@Method()
79-
public async focus() {
80-
return new Promise<void>((resolve) => {
81-
requestAnimationFrame(() => {
82-
this.buttonRef?.focus();
83-
resolve();
84-
});
85-
});
79+
public async focus(host: HTMLElement): Promise<void> {
80+
await propagateFocus(host, this.buttonRef);
8681
}
8782

8883
private readonly hideTooltip = () => {
@@ -118,14 +113,14 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
118113
}
119114

120115
if (this.host) {
121-
dispatchDomEvent(this.host, KolEvent.click, this.state._value);
116+
dispatchDomEvent(this.host as HTMLElement, KolEvent.click, this.state._value);
122117
}
123118
};
124119

125120
private readonly onMouseDown = (event: MouseEvent) => {
126121
this.state?._on?.onMouseDown?.(event);
127122
if (this.host) {
128-
dispatchDomEvent(this.host, KolEvent.mousedown);
123+
dispatchDomEvent(this.host as HTMLElement, KolEvent.mousedown);
129124
}
130125
};
131126

@@ -320,7 +315,7 @@ export class KolButtonWc implements ButtonAPI, FocusableElement {
320315
};
321316

322317
public constructor() {
323-
this.controller = new AssociatedInputController(this, 'button', this.host);
318+
this.controller = new AssociatedInputController(this, 'button', this.host as HTMLElement);
324319
}
325320

326321
@Watch('_accessKey')

packages/components/src/components/button/shadow.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSX } from '@stencil/core';
2-
import { Component, h, Method, Prop } from '@stencil/core';
2+
import { Component, Element, h, Method, Prop } from '@stencil/core';
33
import { KolButtonWcTag } from '../../core/component-names';
44
import type {
55
AccessKeyPropType,
@@ -33,6 +33,7 @@ import type {
3333
shadow: true,
3434
})
3535
export class KolButton implements ButtonProps, FocusableElement {
36+
@Element() private readonly host?: HTMLKolButtonElement;
3637
private buttonWcRef?: HTMLKolButtonWcElement;
3738

3839
private readonly catchRef = (ref?: HTMLKolButtonWcElement) => {
@@ -52,8 +53,8 @@ export class KolButton implements ButtonProps, FocusableElement {
5253
* Sets focus on the internal element.
5354
*/
5455
@Method()
55-
public async focus() {
56-
return Promise.resolve(this.buttonWcRef?.focus());
56+
public async focus(): Promise<void> {
57+
await this.buttonWcRef?.focus(this.host as HTMLElement);
5758
}
5859

5960
public render(): JSX.Element {

packages/components/src/components/combobox/shadow.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
ComboboxAPI,
1515
ComboboxStates,
1616
DisabledPropType,
17+
FocusableElement,
1718
HideLabelPropType,
1819
HideMsgPropType,
1920
HintPropType,
@@ -34,6 +35,7 @@ import type {
3435
import type { EventDetail } from '../../schema/interfaces/EventDetail';
3536
import clsx from '../../utils/clsx';
3637
import { nonce } from '../../utils/dev.utils';
38+
import { propagateFocus } from '../../utils/element-focus';
3739
import { ComboboxController } from './controller';
3840

3941
/**
@@ -46,8 +48,8 @@ import { ComboboxController } from './controller';
4648
},
4749
shadow: true,
4850
})
49-
export class KolCombobox implements ComboboxAPI {
50-
@Element() private readonly host?: HTMLElement;
51+
export class KolCombobox implements ComboboxAPI, FocusableElement {
52+
@Element() private readonly host?: HTMLKolComboboxElement;
5153
private refInput?: HTMLInputElement;
5254
private refSuggestions: HTMLLIElement[] = [];
5355
private _focusedOptionIndex: number = -1;
@@ -67,12 +69,7 @@ export class KolCombobox implements ComboboxAPI {
6769
*/
6870
@Method()
6971
public async focus() {
70-
return new Promise<void>((resolve) => {
71-
requestAnimationFrame(() => {
72-
this.refInput?.focus();
73-
resolve();
74-
});
75-
});
72+
await propagateFocus(this.host, this.refInput);
7673
}
7774

7875
private toggleListbox = () => {

packages/components/src/components/component-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { KolModal } from './modal/shadow';
3636
import { KolNav } from './nav/shadow';
3737
import { KolPagination } from './pagination/shadow';
3838
import { KolPopoverButton } from './popover-button/shadow';
39-
import { KolPopover } from './popover/component';
39+
import { KolPopoverWc } from './popover/component';
4040
import { KolProgress } from './progress/component';
4141
import { KolQuote } from './quote/component';
4242
import { KolSelect } from './select/shadow';
@@ -93,7 +93,7 @@ export const COMPONENTS = [
9393
KolModal,
9494
KolNav,
9595
KolPagination,
96-
KolPopover,
96+
KolPopoverWc,
9797
KolProgress,
9898
KolPopoverButton,
9999
KolQuote,

packages/components/src/components/details/shadow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export class KolDetails implements DetailsAPI, FocusableElement {
3838
* Sets focus on the internal element.
3939
*/
4040
@Method()
41-
public async focus() {
42-
return Promise.resolve(this.buttonWcRef?.focus());
41+
public async focus(): Promise<void> {
42+
await this.buttonWcRef?.focus(this.host as HTMLElement);
4343
}
4444

4545
private toggleTimeout?: ReturnType<typeof setTimeout>;

0 commit comments

Comments
 (0)