Skip to content

Commit 78a6915

Browse files
committed
feat(tree): enhance focus management and caching for tree items
- Added focus method to KolTreeWc to set focus on the first focusable tree item. - Implemented cache invalidation for open tree items to improve performance. - Introduced requestAnimationFrame for debouncing tree change handling. - Updated KolTree to delegate focus to KolTreeWc and handle focus events. - Enhanced keyboard navigation for tree items to improve user experience. - Added end-to-end tests for focus management and performance stability. - Updated snapshots for various components to reflect new focus behavior. - Introduced utility function for delegating focus to ensure elements are ready.
1 parent 8bc7100 commit 78a6915

File tree

61 files changed

+909
-238
lines changed

Some content is hidden

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

61 files changed

+909
-238
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ exports["Component hydration snapshots/renders kol-toolbar with renderToString(0
186186

187187
exports["Component hydration snapshots/renders kol-toolbar with streamToString(0)"] = "<kol-toolbar _label=\"Test kol-toolbar\" s-mode=\"default\" class=\"sc-kol-toolbar-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><div class=\"kol-toolbar kol-toolbar--orientation-horizontal sc-kol-toolbar-default\" role=\"toolbar\" aria-label=\"Test kol-toolbar\"></div></template></kol-toolbar>";
188188

189-
exports["Component hydration snapshots/renders kol-tree with renderToString(0)"] = "<kol-tree _label=\"Test kol-tree\" s-mode=\"default\" class=\"sc-kol-tree-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><kol-tree-wc class=\"sc-kol-tree-default hydrated\"><nav class=\"kol-tree\" aria-label=\"Test kol-tree\"><ul class=\"kol-tree__treeview-navigation\" role=\"tree\" aria-label=\"Test kol-tree\"><slot class=\"sc-kol-tree-default\"></slot></ul></nav></kol-tree-wc></template></kol-tree>";
189+
exports["Component hydration snapshots/renders kol-tree with renderToString(0)"] = "<kol-tree _label=\"Test kol-tree\" s-mode=\"default\" class=\"sc-kol-tree-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><kol-tree-wc class=\"sc-kol-tree-default hydrated\" tabindex=\"0\"><nav class=\"kol-tree\" aria-label=\"Test kol-tree\"><ul class=\"kol-tree__treeview-navigation\" role=\"tree\" aria-label=\"Test kol-tree\"><slot class=\"sc-kol-tree-default\"></slot></ul></nav></kol-tree-wc></template></kol-tree>";
190190

191-
exports["Component hydration snapshots/renders kol-tree with streamToString(0)"] = "<kol-tree _label=\"Test kol-tree\" s-mode=\"default\" class=\"sc-kol-tree-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><kol-tree-wc class=\"sc-kol-tree-default hydrated\"><nav class=\"kol-tree\" aria-label=\"Test kol-tree\"><ul class=\"kol-tree__treeview-navigation\" role=\"tree\" aria-label=\"Test kol-tree\"><slot class=\"sc-kol-tree-default\"></slot></ul></nav></kol-tree-wc></template></kol-tree>";
191+
exports["Component hydration snapshots/renders kol-tree with streamToString(0)"] = "<kol-tree _label=\"Test kol-tree\" s-mode=\"default\" class=\"sc-kol-tree-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><kol-tree-wc class=\"sc-kol-tree-default hydrated\" tabindex=\"0\"><nav class=\"kol-tree\" aria-label=\"Test kol-tree\"><ul class=\"kol-tree__treeview-navigation\" role=\"tree\" aria-label=\"Test kol-tree\"><slot class=\"sc-kol-tree-default\"></slot></ul></nav></kol-tree-wc></template></kol-tree>";
192192

193193
exports["Component hydration snapshots/renders kol-tree-item with renderToString(0)"] = "<kol-tree-item _href=\"Test value\" _label=\"Test kol-tree-item\" s-mode=\"default\" class=\"sc-kol-tree-item-default-h hydrated\" style=\"visibility: hidden;\"><template shadowrootmode=\"open\"><style>/* CSS normalized */</style><kol-tree-item-wc class=\"sc-kol-tree-item-default hydrated\"><li class=\"kol-tree-item\" style=\"--level: 0;\"><kol-link-wc class=\"kol-tree-item__link kol-tree-item__link--first-level hydrated\"><a href=\"Test value\" class=\"kol-link kol-link--inline\" role=\"treeitem\" tabindex=\"-1\"><span class=\"kol-span kol-link__text\"><span class=\"kol-span__container\"><span class=\"kol-span__label\"><span class=\"kol-tree-item__link-inner\" slot=\"expert\"><span class=\"kol-tree-item__toggle-button-placeholder\"></span><span class=\"kol-tree-item__text\">Test kol-tree-item</span></span></span></span></span></a></kol-link-wc><ul class=\"kol-tree-item__children\" hidden role=\"group\" id=\"[id]\"><slot class=\"sc-kol-tree-item-default\"></slot></ul></li></kol-tree-item-wc></template></kol-tree-item>";
194194

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ 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+
if (this.host) {
57+
await this.buttonWcRef?.focus(this.host);
58+
}
5759
}
5860

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

packages/components/src/components/alert/component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class KolAlertWc implements AlertAPI {
2929
private readonly close = () => {
3030
this._on?.onClose?.(new Event('Close'));
3131
if (this.host) {
32-
dispatchDomEvent(this.host as HTMLElement, KolEvent.close);
32+
dispatchDomEvent(this.host, KolEvent.close);
3333
}
3434
};
3535

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

Lines changed: 5 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,9 @@ export class KolBadge implements BadgeAPI, FocusableElement {
5657
*/
5758
@Method()
5859
public async focus(): Promise<void> {
59-
return Promise.resolve(this.smartButtonRef?.focus());
60+
if (this.host) {
61+
return this.smartButtonRef?.focus(this.host);
62+
}
6063
}
6164

6265
public render(): JSX.Element {

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

Lines changed: 6 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,10 @@ 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+
if (this.host) {
66+
await this.buttonWcRef?.focus(this.host);
67+
}
6568
}
6669

6770
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 { delegateFocus } 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 delegateFocus(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: 6 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,10 @@ 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+
if (this.host) {
58+
await this.buttonWcRef?.focus(this.host);
59+
}
5760
}
5861

5962
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 { delegateFocus } 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 delegateFocus(this.host, this.refInput);
7673
}
7774

7875
private toggleListbox = () => {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ 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+
if (this.host) {
43+
await this.buttonWcRef?.focus(this.host);
44+
}
4345
}
4446

4547
private toggleTimeout?: ReturnType<typeof setTimeout>;

packages/components/src/components/form/shadow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { dispatchDomEvent, KolEvent } from '../../utils/events';
2222
shadow: true,
2323
})
2424
export class KolForm implements FormAPI {
25-
@Element() private readonly host?: HTMLKolTextareaElement;
25+
@Element() private readonly host?: HTMLKolFormElement;
2626
errorListBlock?: HTMLElement;
2727
errorListFirstLink?: HTMLElement;
2828
private readonly translateErrorListMessage = translate('kol-error-list-message');
@@ -61,7 +61,7 @@ export class KolForm implements FormAPI {
6161

6262
private readonly setBlockElement = (el?: HTMLElement) => (this.errorListBlock = el);
6363

64-
private readonly setFirstLinkElement = (el?: HTMLElement) => (this.errorListFirstLink = el);
64+
private readonly setFirstLinkElement = (el?: HTMLKolLinkWcElement) => (this.errorListFirstLink = el as HTMLElement);
6565

6666
private renderErrorList(errorList?: ErrorListPropType[]): JSX.Element {
6767
return (

0 commit comments

Comments
 (0)