Skip to content

Commit de3ab96

Browse files
authored
feat(ui5-timeline): improve keyboard handling (#12021)
ui5-timeline keyboard handling improvements: - press F2 on focused timeline item will move the focus to the first interactive element inside the timeline item - press F2 on focused interactive element inside timeline item will move the focus to the parent timeline item - press Tab on interactive element inside timeline item will move the focus to the next interactive element inside the same timeline item if such element exists otherwise will move the focus to closest interactive element in timeline chain
1 parent f1ce974 commit de3ab96

File tree

2 files changed

+129
-32
lines changed

2 files changed

+129
-32
lines changed

packages/fiori/cypress/specs/Timeline.cy.tsx

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import messageInformation from "@ui5/webcomponents-icons/dist/message-informatio
77
import Label from "@ui5/webcomponents/dist/Label.js";
88
import Avatar from "@ui5/webcomponents/dist/Avatar.js";
99
import UI5Element from "@ui5/webcomponents-base";
10+
import Button from "@ui5/webcomponents/dist/Button.js";
1011
import Input from "@ui5/webcomponents/dist/Input.js";
1112

1213
function Sample() {
@@ -134,7 +135,9 @@ describe("Timeline with group items interactions", () => {
134135
cy.get("@currentGroupButton")
135136
.realClick();
136137

137-
cy.realPress("Tab");
138+
cy.realPress("F2");
139+
140+
cy.realPress("ArrowDown");
138141

139142
cy.get("[ui5-timeline]")
140143
.find("[ui5-timeline-group-item][group-name='Meetings']")
@@ -299,6 +302,113 @@ describe("Timeline with growing mode", () => {
299302
});
300303
});
301304

305+
describe("Keyboard interactions", () => {
306+
it("F2 should move the focus to interactive items and then should revert the focus to parent timeline item", () => {
307+
cy.mount(
308+
<Timeline>
309+
<TimelineItem titleText="first item" subtitleText="20.02.2017 11:30" >
310+
<Button id="button" title="Click me"></Button>
311+
</TimelineItem>
312+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30"></TimelineItem>
313+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30" ></TimelineItem>
314+
</Timeline>
315+
);
316+
317+
cy.get("[ui5-timeline]")
318+
.as("timeline");
319+
320+
cy.get("@timeline")
321+
.find("ui5-timeline-item")
322+
.first()
323+
.as("firstItem")
324+
.realClick();
325+
326+
cy.get("@firstItem")
327+
.should("be.focused");
328+
329+
cy.realPress("F2");
330+
331+
cy.get("#button")
332+
.should("be.focused");
333+
334+
cy.realPress("F2");
335+
336+
cy.get("@firstItem")
337+
.should("be.focused");
338+
});
339+
340+
it("should move the focus to the next interactive item inside timeline item when Tab is pressed", () => {
341+
cy.mount(
342+
<Timeline>
343+
<TimelineItem titleText="first item" subtitleText="20.02.2017 11:30" >
344+
<Button id="button1" title="Click me"></Button>
345+
<Button id="button2" title="Click me"></Button>
346+
</TimelineItem>
347+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30"></TimelineItem>
348+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30" ></TimelineItem>
349+
</Timeline>
350+
);
351+
352+
cy.get("[ui5-timeline]")
353+
.as("timeline");
354+
355+
cy.get("@timeline")
356+
.find("ui5-timeline-item")
357+
.first()
358+
.as("firstItem")
359+
.realClick();
360+
361+
cy.get("@firstItem")
362+
.should("be.focused");
363+
364+
cy.realPress("F2");
365+
366+
cy.get("#button1")
367+
.should("be.focused");
368+
369+
cy.realPress("Tab");
370+
371+
cy.get("#button2")
372+
.should("be.focused");
373+
});
374+
375+
it("should move the focus to interactive element inside next timeline item with interactive elements when pressing Tab", () => {
376+
cy.mount(
377+
<Timeline>
378+
<TimelineItem titleText="first item" subtitleText="20.02.2017 11:30" >
379+
<Button id="button1" title="Click me"></Button>
380+
</TimelineItem>
381+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30"></TimelineItem>
382+
<TimelineItem titleText="coming up" subtitleText="20.02.2017 11:30">
383+
<Button id="button2" title="Click me"></Button>
384+
</TimelineItem>
385+
</Timeline>
386+
);
387+
388+
cy.get("[ui5-timeline]")
389+
.as("timeline");
390+
391+
cy.get("@timeline")
392+
.find("ui5-timeline-item")
393+
.first()
394+
.as("firstItem")
395+
.realClick();
396+
397+
cy.get("@firstItem")
398+
.should("be.focused");
399+
400+
cy.realPress("F2");
401+
402+
cy.get("#button1")
403+
.should("be.focused");
404+
405+
cy.realPress("Tab");
406+
407+
cy.get("#button2")
408+
.should("be.focused");
409+
});
410+
});
411+
302412
describe("Accessibility", () => {
303413
beforeEach(() => {
304414
cy.mount(

packages/fiori/src/Timeline.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
77
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
88
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
99
import {
10-
isTabNext,
11-
isTabPrevious,
1210
isSpace,
1311
isEnter,
1412
isUp,
1513
isDown,
1614
isLeft,
1715
isRight,
16+
isF2,
1817
} from "@ui5/webcomponents-base/dist/Keys.js";
1918
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
2019
import type ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js";
@@ -33,6 +32,8 @@ import TimelineCss from "./generated/themes/Timeline.css.js";
3332
import TimelineLayout from "./types/TimelineLayout.js";
3433
// Mode
3534
import TimelineGrowingMode from "./types/TimelineGrowingMode.js";
35+
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
36+
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
3637

3738
/**
3839
* Interface for components that may be slotted inside `ui5-timeline` as items
@@ -353,7 +354,7 @@ class Timeline extends UI5Element {
353354
}
354355
}
355356

356-
_onkeydown(e: KeyboardEvent) {
357+
async _onkeydown(e: KeyboardEvent) {
357358
const target = e.target as ITimelineItem,
358359
targetfocusDomRef = target?.getFocusDomRef(),
359360
shouldHandleCustomArrowNavigation = targetfocusDomRef === this.getFocusDomRef() || target === this.growingButton;
@@ -370,36 +371,22 @@ class Timeline extends UI5Element {
370371
return;
371372
}
372373

373-
if (target.nameClickable && !target.getFocusDomRef()!.matches(":has(:focus-within)")) {
374-
return;
375-
}
376-
377-
if (isTabNext(e)) {
378-
this._handleNextOrPreviousItem(e, true);
379-
} else if (isTabPrevious(e)) {
380-
this._handleNextOrPreviousItem(e);
381-
}
382-
}
383-
384-
_handleNextOrPreviousItem(e: KeyboardEvent, isNext?: boolean) {
385-
const target = e.target as ITimelineItem | ToggleButton;
386-
let updatedTarget = target;
387-
388-
if ((target as ITimelineItem).isGroupItem) {
389-
updatedTarget = target.shadowRoot!.querySelector<ToggleButton>("[ui5-toggle-button]")!;
390-
}
391-
392-
const nextTargetIndex = isNext ? this._navigableItems.indexOf(updatedTarget) + 1 : this._navigableItems.indexOf(updatedTarget) - 1;
393-
const nextTarget = this._navigableItems[nextTargetIndex];
374+
if (isF2(e)) {
375+
e.stopImmediatePropagation();
376+
const activeElement = getActiveElement();
377+
const focusDomRef = this.getFocusDomRef();
394378

395-
if (!nextTarget) {
396-
return;
397-
}
379+
if (!focusDomRef) {
380+
return;
381+
}
398382

399-
if (nextTarget) {
400-
e.preventDefault();
401-
nextTarget.focus();
402-
this._itemNavigation.setCurrentItem(nextTarget);
383+
if (activeElement === focusDomRef) {
384+
const firstFocusable = await getFirstFocusableElement(focusDomRef);
385+
firstFocusable?.focus();
386+
} else {
387+
const parentItem = (e.target as HTMLElement)?.closest("ui5-timeline-item") as HTMLElement;
388+
parentItem?.focus();
389+
}
403390
}
404391
}
405392

0 commit comments

Comments
 (0)