From a526c35ba8f3f6c904b05ae823e4a27b773ee54f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beril=20G=C3=B6k=C3=A7e=20=C3=87i=C3=A7ek?=
<52400850+gokcecicek@users.noreply.github.com>
Date: Thu, 9 May 2024 14:10:11 +0300
Subject: [PATCH] feat(select): improve option navigation by key (#856)
This PR allows users to easily access target option based on their
keyboard input.
Closes [#686](https://github.com/Trendyol/baklava/issues/686)
---
src/components/select/bl-select.stories.mdx | 1 +
src/components/select/bl-select.test.ts | 101 ++++++++++++++++++++
src/components/select/bl-select.ts | 31 ++++++
3 files changed, 133 insertions(+)
diff --git a/src/components/select/bl-select.stories.mdx b/src/components/select/bl-select.stories.mdx
index 7a408ef8..01bc9860 100644
--- a/src/components/select/bl-select.stories.mdx
+++ b/src/components/select/bl-select.stories.mdx
@@ -146,6 +146,7 @@ ${SelectTemplate(args)}
# Select
[ADR](https://github.com/Trendyol/baklava/issues/88)
+[Accessibility](https://github.com/Trendyol/baklava/issues/686#issuecomment-2079522703)
[Figma](https://www.figma.com/file/RrcLH0mWpIUy4vwuTlDeKN/Baklava-Design-Guide?node-id=25%3A3606)
Select component is a component for selecting a value from a list of options.
diff --git a/src/components/select/bl-select.test.ts b/src/components/select/bl-select.test.ts
index 215ea5a3..a4417b7a 100644
--- a/src/components/select/bl-select.test.ts
+++ b/src/components/select/bl-select.test.ts
@@ -632,6 +632,8 @@ describe("bl-select", () => {
Basketball
Football
Tennis
+ Boxing
+ Hockey
`);
@@ -758,6 +760,105 @@ describe("bl-select", () => {
//then
expect((document.activeElement as BlSelectOption).value).to.equal(firstOption?.value);
});
+
+ it("should focus the first matching option when typing a single character", async () => {
+ const firstOption = el.querySelector("bl-select-option");
+
+ //given
+ await sendKeys({
+ press: tabKey,
+ });
+ await sendKeys({
+ press: "Space",
+ });
+ await sendKeys({
+ press: "b",
+ });
+
+ //then
+ expect((document.activeElement as BlSelectOption).value).to.equal(firstOption?.value);
+ });
+
+ it("should focus the first matching option when typing a single character with uppercase", async () => {
+ const firstOption = el.querySelector("bl-select-option");
+
+ //given
+ await sendKeys({
+ press: tabKey,
+ });
+ await sendKeys({
+ press: "Space",
+ });
+ await sendKeys({
+ press: "B",
+ });
+
+ //then
+ expect((document.activeElement as BlSelectOption).value).to.equal(firstOption?.value);
+ });
+
+ it("should focus the first matching option when typing two characters", async () => {
+ const fourthOption = el.querySelector("bl-select-option:nth-child(4)");
+
+ //given
+ await sendKeys({
+ press: tabKey,
+ });
+ await sendKeys({
+ press: "Space",
+ });
+ await sendKeys({
+ press: "b",
+ });
+ await sendKeys({
+ press: "o",
+ });
+
+ //then
+ expect((document.activeElement as BlSelectOption).value).to.equal(fourthOption?.value);
+ });
+
+ it("should reset typed characters after an interval of inactivity", async () => {
+ const secondOption = el.querySelector("bl-select-option:nth-child(2)");
+
+ // when
+ await sendKeys({
+ press: tabKey,
+ });
+ await sendKeys({
+ press: "Space",
+ });
+ await sendKeys({
+ press: "b",
+ });
+ // Wait for an interval of inactivity
+ await new Promise(resolve => setTimeout(resolve, 600));
+
+ await sendKeys({
+ press: "f",
+ });
+
+ //then
+ expect((document.activeElement as BlSelectOption).value).to.equal(secondOption?.value);
+ });
+
+ it("should not focus on the disabled option even if it matches the typed character", async () => {
+ const focusedOptions = el.querySelectorAll("bl-select-option:focus");
+
+ //given
+ await sendKeys({
+ press: tabKey,
+ });
+ await sendKeys({
+ press: "Space",
+ });
+ await sendKeys({
+ press: "h",
+ });
+
+ //then
+ expect(focusedOptions.length).to.equal(0);
+ });
});
describe("select all", () => {
diff --git a/src/components/select/bl-select.ts b/src/components/select/bl-select.ts
index d8ec4c22..c6063879 100644
--- a/src/components/select/bl-select.ts
+++ b/src/components/select/bl-select.ts
@@ -571,6 +571,35 @@ export default class BlSelect extends Form
}
private focusedOptionIndex = -1;
+ private lastKeyPressedTime = 0;
+ private typedCharacters = "";
+ private keyPressThreshold = 500;
+
+ private handleFocusOptionByKey(key: string) {
+ const currentTime = Date.now();
+ const elapsedTimeSinceLastKeyPress = currentTime - this.lastKeyPressedTime;
+
+ if (elapsedTimeSinceLastKeyPress > this.keyPressThreshold) {
+ this.typedCharacters = "";
+ }
+
+ this.lastKeyPressedTime = currentTime;
+ this.typedCharacters += key.toLowerCase();
+
+ const matchingOptionIndex = this.options.findIndex(option => {
+ if (option.disabled) {
+ return false;
+ }
+ const optionText = option.innerText.trim().toLowerCase();
+
+ return optionText.startsWith(this.typedCharacters);
+ });
+
+ if (matchingOptionIndex !== -1) {
+ this.focusedOptionIndex = matchingOptionIndex;
+ this.options[matchingOptionIndex].focus();
+ }
+ }
private handleKeydown(event: KeyboardEvent) {
if (this.focusedOptionIndex === -1 && ["Enter", "Space"].includes(event.code)) {
@@ -595,6 +624,8 @@ export default class BlSelect extends Form
this.options[this.focusedOptionIndex].focus();
event.preventDefault();
+ } else if (this._isPopoverOpen && !this.searchBar) {
+ this.handleFocusOptionByKey(event.key);
}
}