diff --git a/packages/base/src/features/F6Navigation.ts b/packages/base/src/features/F6Navigation.ts index b9b1aa188155..3f1831bf8cee 100644 --- a/packages/base/src/features/F6Navigation.ts +++ b/packages/base/src/features/F6Navigation.ts @@ -3,6 +3,7 @@ import { isF6Next, isF6Previous } from "../Keys.js"; import { instanceOfUI5Element } from "../UI5Element.js"; import { getFirstFocusableElement } from "../util/FocusableElements.js"; import getFastNavigationGroups from "../util/getFastNavigationGroups.js"; +import isElementClickable from "../util/isElementClickable.js"; class F6Navigation { static _instance: F6Navigation; @@ -19,56 +20,61 @@ class F6Navigation { document.addEventListener("keydown", this.keydownHandler); } - async _keydownHandler(event: KeyboardEvent) { - if (isF6Next(event)) { - this.updateGroups(); + async groupElementToFocus(nextElement: HTMLElement) { + const nextElementDomRef = instanceOfUI5Element(nextElement) ? nextElement.getDomRef() : nextElement; - if (this.groups.length < 1) { - return; + if (nextElementDomRef) { + if (isElementClickable(nextElementDomRef)) { + return nextElementDomRef; } - event.preventDefault(); + const elementToFocus = await getFirstFocusableElement(nextElementDomRef); - let nextIndex = -1; - let nextElement; - if (this.selectedGroup) { - nextIndex = this.groups.indexOf(this.selectedGroup); + if (elementToFocus) { + return elementToFocus; } + } + } + + async findNextFocusableGroupElement(currentIndex: number) { + let elementToFocus; - if (nextIndex > -1) { - if (nextIndex + 1 >= this.groups.length) { - nextElement = this.groups[0]; + /* eslint-disable no-await-in-loop */ + for (let index = 0; index < this.groups.length; index++) { + let nextElement; + + if (currentIndex > -1) { + if (currentIndex + 1 >= this.groups.length) { + currentIndex = 0; + nextElement = this.groups[currentIndex]; } else { - nextElement = this.groups[nextIndex + 1]; + currentIndex += 1; + nextElement = this.groups[currentIndex]; } } else { - nextElement = this.groups[0]; + currentIndex = 0; + nextElement = this.groups[currentIndex]; } - const nextElementDomRef = instanceOfUI5Element(nextElement) ? nextElement.getDomRef() : nextElement; + elementToFocus = await this.groupElementToFocus(nextElement); - if (nextElementDomRef) { - const elementToFocus = await getFirstFocusableElement(nextElementDomRef, true); - elementToFocus?.focus(); + if (elementToFocus) { + break; } } + /* eslint-enable no-await-in-loop */ - if (isF6Previous(event)) { - this.updateGroups(); - - if (this.groups.length < 1) { - return; - } + return elementToFocus; + } - event.preventDefault(); + async findPreviousFocusableGroupElement(currentIndex: number) { + let elementToFocus; - let nextIndex = -1; + /* eslint-disable no-await-in-loop */ + for (let index = 0; index < this.groups.length; index++) { let nextElement; - if (this.selectedGroup) { - nextIndex = this.groups.indexOf(this.selectedGroup); - } - if (nextIndex > 0) { + if (currentIndex > 0) { // Handle the situation where the first focusable element of two neighbor groups is the same // For example: // @@ -77,22 +83,70 @@ class F6Navigation { // // // Here for both FCL & List the firstFoccusableElement is the same (the ui5-li) + const firstFocusable = await this.groupElementToFocus(this.groups[currentIndex - 1]); + const shouldSkipParent = firstFocusable === await this.groupElementToFocus(this.groups[currentIndex]); - const firstFocusable = await getFirstFocusableElement(this.groups[nextIndex - 1], true); - const shouldSkipParent = firstFocusable === await getFirstFocusableElement(this.groups[nextIndex], true); + currentIndex = shouldSkipParent ? currentIndex - 2 : currentIndex - 1; - nextElement = this.groups[shouldSkipParent ? nextIndex - 2 : nextIndex - 1]; + if (currentIndex < 0) { + currentIndex = this.groups.length - 1; + } + + nextElement = this.groups[currentIndex]; } else { - nextElement = this.groups[this.groups.length - 1]; + currentIndex = this.groups.length - 1; + nextElement = this.groups[currentIndex]; } - const nextElementDomRef = instanceOfUI5Element(nextElement) ? nextElement.getDomRef() : nextElement; + elementToFocus = await this.groupElementToFocus(nextElement); - if (nextElementDomRef) { - const elementToFocus = await getFirstFocusableElement(nextElementDomRef, true); - elementToFocus?.focus(); + if (elementToFocus) { + break; } } + /* eslint-enable no-await-in-loop */ + + return elementToFocus; + } + + async _keydownHandler(event: KeyboardEvent) { + const forward = isF6Next(event); + const backward = isF6Previous(event); + if (!(forward || backward)) { + return; + } + + this.updateGroups(); + + if (this.groups.length < 1) { + return; + } + + event.preventDefault(); + + let elementToFocus; + + if (this.groups.length === 0) { + elementToFocus = await this.groupElementToFocus(this.groups[0]); + + return elementToFocus?.focus(); + } + + let currentIndex = -1; + + if (this.selectedGroup) { + currentIndex = this.groups.indexOf(this.selectedGroup); + } + + if (forward) { + elementToFocus = await this.findNextFocusableGroupElement(currentIndex); + } + + if (backward) { + elementToFocus = await this.findPreviousFocusableGroupElement(currentIndex); + } + + elementToFocus?.focus(); } removeEventListeners() { @@ -109,19 +163,19 @@ class F6Navigation { let element: Element | null | ParentNode = this.deepActive(root); while (element && (element as Element).getAttribute("data-sap-ui-fastnavgroup") !== "true" && element !== htmlElement) { - element = element.parentElement ? element.parentNode : (element.parentNode as ShadowRoot).host; + element = element.parentElement ? element.parentNode : (element.parentNode as ShadowRoot).host; } this.selectedGroup = element as HTMLElement; - } + } - deepActive(root: DocumentOrShadowRoot): Element | null { + deepActive(root: DocumentOrShadowRoot): Element | null { if (root.activeElement && root.activeElement.shadowRoot) { - return this.deepActive(root.activeElement.shadowRoot); + return this.deepActive(root.activeElement.shadowRoot); } return root.activeElement; - } + } destroy() { this.removeEventListeners(); diff --git a/packages/main/test/pages/F6Test1.html b/packages/main/test/pages/F6Test1.html new file mode 100644 index 000000000000..bdf2744dde14 --- /dev/null +++ b/packages/main/test/pages/F6Test1.html @@ -0,0 +1,40 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ First focusable +
+
+ Something focusable +
+
+ Second focusable +
+
+ Something focusable +
+
+ Third focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test2.html b/packages/main/test/pages/F6Test2.html new file mode 100644 index 000000000000..6b7887ffee99 --- /dev/null +++ b/packages/main/test/pages/F6Test2.html @@ -0,0 +1,40 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ First focusable +
+
+ Something focusable +
+
+ Group without focusable element +
+
+ Something focusable +
+
+ Second focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test3.html b/packages/main/test/pages/F6Test3.html new file mode 100644 index 000000000000..422926fca16e --- /dev/null +++ b/packages/main/test/pages/F6Test3.html @@ -0,0 +1,37 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ First focusable +
+ Second focusable +
+
+
+ Something focusable +
+
+ Third focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test4.html b/packages/main/test/pages/F6Test4.html new file mode 100644 index 000000000000..1e9e280dc0aa --- /dev/null +++ b/packages/main/test/pages/F6Test4.html @@ -0,0 +1,44 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+
+ First focusable +
+
+
+ Something focusable +
+
+
+ First focusable +
+
+
+ Something focusable +
+
+ Second focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test5.html b/packages/main/test/pages/F6Test5.html new file mode 100644 index 000000000000..67780f36e47a --- /dev/null +++ b/packages/main/test/pages/F6Test5.html @@ -0,0 +1,40 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ First focusable +
+
+ Something focusable +
+
+ Second focusable +
+
+ Something focusable +
+
+ Third focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test6.html b/packages/main/test/pages/F6Test6.html new file mode 100644 index 000000000000..818d4ec2bb47 --- /dev/null +++ b/packages/main/test/pages/F6Test6.html @@ -0,0 +1,37 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ Group without focusable element +
+
+ Something focusable +
+
+ Group without focusable element +
+
+ Something focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/F6Test7.html b/packages/main/test/pages/F6Test7.html new file mode 100644 index 000000000000..40cafaa5f27a --- /dev/null +++ b/packages/main/test/pages/F6Test7.html @@ -0,0 +1,34 @@ + + + + + + + Avatar + + + + + + + + + +
+ Before element +
+
+ Before element +
+
+ Something focusable +
+
+ Something focusable +
+
+ After Element +
+ + + \ No newline at end of file diff --git a/packages/main/test/pages/Test.html b/packages/main/test/pages/Test.html new file mode 100644 index 000000000000..47e56cd78d72 --- /dev/null +++ b/packages/main/test/pages/Test.html @@ -0,0 +1,15 @@ + + + + + + Table + + + + + + + + + diff --git a/packages/main/test/pages/styles/F6Test.css b/packages/main/test/pages/styles/F6Test.css new file mode 100644 index 000000000000..f1d68ece0032 --- /dev/null +++ b/packages/main/test/pages/styles/F6Test.css @@ -0,0 +1,9 @@ +.section { + margin: 10px 0; + padding: 10px; + border: 1px solid red; +} +[data-sap-ui-fastnavgroup] { + border-width: 3px; + border-color: green; +} \ No newline at end of file diff --git a/packages/main/test/specs/F6Test.spec.js b/packages/main/test/specs/F6Test.spec.js new file mode 100644 index 000000000000..55480570e61f --- /dev/null +++ b/packages/main/test/specs/F6Test.spec.js @@ -0,0 +1,242 @@ +const assert = require("chai").assert; + + +describe("F6 Test", () => { + describe("Forward", () => { + it("Basic", async () => { + await browser.url(`test/pages/F6Test1.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Basic with an empty group", async () => { + await browser.url(`test/pages/F6Test2.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Nested groups", async () => { + await browser.url(`test/pages/F6Test3.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Nested inside empty fastnav-group parent", async () => { + await browser.url(`test/pages/F6Test4.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct "); + }); + + it("Basic with group as focusable element", async () => { + await browser.url(`test/pages/F6Test5.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Groups without focusable element", async () => { + await browser.url(`test/pages/F6Test6.html`); + + const button = await browser.$("#first"); + + await button.click(); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("One group", async () => { + await browser.url(`test/pages/F6Test7.html`); + + // Go to next group + await browser.keys("F6"); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + }) + + describe("Backward", () => { + it("Basic", async () => { + await browser.url(`test/pages/F6Test1.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Basic with an empty group", async () => { + await browser.url(`test/pages/F6Test2.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Nested groups", async () => { + await browser.url(`test/pages/F6Test3.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Nested inside empty fastnav-group parent", async () => { + await browser.url(`test/pages/F6Test4.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct "); + }); + + it("Basic with group as focusable element", async () => { + await browser.url(`test/pages/F6Test5.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("second", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to first group (circle) + await browser.keys(["Shift", "F6"]); + assert.equal("third", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("Groups without focusable element", async () => { + await browser.url(`test/pages/F6Test6.html`); + + const button = await browser.$("#first"); + + await button.click(); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + + it("One group", async () => { + await browser.url(`test/pages/F6Test7.html`); + + // Go to next group + await browser.keys(["Shift", "F6"]); + assert.equal("first", await browser.$(await browser.getActiveElement()).getAttribute("id"), "correct focus"); + }); + }) +});