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); } }