From b914faf2a40473776dba529736f3b7b37e138ecc Mon Sep 17 00:00:00 2001 From: Volubyl Date: Wed, 15 Feb 2023 17:18:43 +0100 Subject: [PATCH] fix: Format Selector Improvements (#566) * WIP * fix: data format field UI imporvements * fix: listbox had no focus --- .../DatasetForm/_FormatSelector.spec.ts | 232 ++++++++++++++++++ .../DatasetForm/_FormatSelector.svelte | 5 +- .../SearchableComboBox.spec.ts | 6 +- .../SearcheableComboBox.svelte | 37 ++- .../src/routes/(app)/contribuer/+page.svelte | 2 +- 5 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 client/src/lib/components/DatasetForm/_FormatSelector.spec.ts diff --git a/client/src/lib/components/DatasetForm/_FormatSelector.spec.ts b/client/src/lib/components/DatasetForm/_FormatSelector.spec.ts new file mode 100644 index 00000000..a5e8bb81 --- /dev/null +++ b/client/src/lib/components/DatasetForm/_FormatSelector.spec.ts @@ -0,0 +1,232 @@ +/** + * @jest-environment jsdom + */ +import "@testing-library/jest-dom"; +import { fireEvent, render } from "@testing-library/svelte"; +import type { DataFormat } from "src/definitions/dataformat"; +import FormatSelector from "./_FormatSelector.svelte"; + +const formatOptions: DataFormat[] = [ + { + name: "label-1", + id: 1, + }, + { + name: "label-2", + id: 2, + }, + { + name: "label-3", + id: 3, + }, +]; + +describe("Test the select component", () => { + test("should display a select input with 3 options", () => { + const props = { + formatOptions, + }; + + const { getAllByRole } = render(FormatSelector, { props }); + + expect(getAllByRole("listbox").length).toBe(1); + }); + + test("should display a select input with 3 options after start to type", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "label", + }, + }); + expect(getAllByRole("option").length).toBe(3); + }); + + test("should display a select input with 3 options after hitting alt + down arrow", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + altKey: true, + }); + expect(getAllByRole("option").length).toBe(3); + }); + + test("should display a select input with 3 options after start typing and hitting down arrow key", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "label", + }, + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + expect(getAllByRole("option").length).toBe(3); + }); + + test("should display a select input with 3 options after start typing and hitting down arrow key", async () => { + const props = { + formatOptions, + }; + + const { getByRole, queryByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "label", + }, + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "Escape", + }); + expect(queryByRole("option")).toBe(null); + }); + + test("should select an option after hitting Enter", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "lab", + }, + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "Enter", + }); + + const tags = getAllByRole("listitem"); + + expect(tags).toHaveLength(1); + + expect(tags[0]).toHaveTextContent("label-1", { + normalizeWhitespace: true, + }); + + expect(combobox).toHaveValue(""); + + expect(combobox).toHaveValue(""); + }); + + test("should select the second option after hitting down arrow twice and Enter", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "lab", + }, + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "Enter", + }); + const tags = getAllByRole("listitem"); + + expect(tags).toHaveLength(1); + + expect(tags[0]).toHaveTextContent("label-2", { + normalizeWhitespace: true, + }); + + expect(combobox).toHaveValue(""); + }); + + test("should select the second option after hitting down arrow 3 times and Enter", async () => { + const props = { + formatOptions, + }; + + const { getByRole, getAllByRole } = render(FormatSelector, { props }); + + const combobox = getByRole("combobox"); + + await fireEvent.input(combobox, { + target: { + value: "label-2", + }, + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "ArrowDown", + }); + + await fireEvent.keyDown(combobox, { + key: "Enter", + }); + + const tags = getAllByRole("listitem"); + + expect(tags).toHaveLength(1); + + expect(tags[0]).toHaveTextContent("label-2", { + normalizeWhitespace: true, + }); + + expect(combobox).toHaveValue(""); + }); +}); diff --git a/client/src/lib/components/DatasetForm/_FormatSelector.svelte b/client/src/lib/components/DatasetForm/_FormatSelector.svelte index e34f246e..90034695 100644 --- a/client/src/lib/components/DatasetForm/_FormatSelector.svelte +++ b/client/src/lib/components/DatasetForm/_FormatSelector.svelte @@ -14,7 +14,7 @@ }>(); export let formatOptions: DataFormat[]; - export let error: string; + export let error = ""; export let selectedFormatOptions: Partial[] = []; @@ -63,13 +63,12 @@ on:selectOption={handleSelectFormat} /> -
+
{#each selectedFormatOptions as format, index} {#if format.name} {/if} diff --git a/client/src/lib/components/SearchableComboBox/SearchableComboBox.spec.ts b/client/src/lib/components/SearchableComboBox/SearchableComboBox.spec.ts index 215f8680..15a7ea2d 100644 --- a/client/src/lib/components/SearchableComboBox/SearchableComboBox.spec.ts +++ b/client/src/lib/components/SearchableComboBox/SearchableComboBox.spec.ts @@ -152,7 +152,7 @@ describe("Test the select component", () => { key: "Enter", }); - expect(combobox).toHaveValue("label-1"); + expect(combobox).toHaveValue(""); }); test("should select the second option after hitting down arrow twice and Enter", async () => { @@ -185,7 +185,7 @@ describe("Test the select component", () => { key: "Enter", }); - expect(combobox).toHaveValue("label-2"); + expect(combobox).toHaveValue(""); }); test("should select the second option after hitting down arrow 3 times and Enter", async () => { @@ -226,6 +226,6 @@ describe("Test the select component", () => { key: "Enter", }); - expect(combobox).toHaveValue("label-1"); + expect(combobox).toHaveValue(""); }); }); diff --git a/client/src/lib/components/SearchableComboBox/SearcheableComboBox.svelte b/client/src/lib/components/SearchableComboBox/SearcheableComboBox.svelte index a31f44e8..79b654f1 100644 --- a/client/src/lib/components/SearchableComboBox/SearcheableComboBox.svelte +++ b/client/src/lib/components/SearchableComboBox/SearcheableComboBox.svelte @@ -19,9 +19,11 @@ let disableAddItem = true; let suggestionList: HTMLElement; let currentLiIndex = 0; - let showSuggestions = false; + let selectedOption: SelectOption; let textBoxHasFocus = false; + let listBoxHasFocus = false; + let showSuggestions = false; $: regexp = value ? new RegExp(escape(value), "i") : null; @@ -29,6 +31,10 @@ ? options.filter((item) => (regexp ? item.label.match(regexp) : true)) : []; + $: if (options.length === 0) { + showSuggestions = false; + } + const getSelectedOption = (value: string): SelectOption | undefined => filteredSuggestions.find((item) => item.label === value.trim()); @@ -41,7 +47,7 @@ const foundOption = getSelectedOption(optionValue); if (foundOption) { - value = foundOption.label; + value = ""; handleSelectOption(foundOption); } showSuggestions = false; @@ -79,6 +85,11 @@ value = ""; }; + const handleInputFocused = () => { + showSuggestions = true; + textBoxHasFocus = true; + }; + const manageKeyboardInterractions = (e: KeyboardEvent) => { let suggestionItems = suggestionList.childNodes; @@ -124,7 +135,7 @@ case "ArrowDown": // If the textbox is not empty and the listbox is displayed, moves visual focus to the first suggested value. textBoxHasFocus = false; - + listBoxHasFocus = true; if (value && showSuggestions) { currentLiIndex = 0; } @@ -139,7 +150,6 @@ break; case "ArrowUp": - // console.log("tata"); // if the textbox is not empty and the listbox is displayed, moves visual focus to the last suggested value. if (!value && showSuggestions) { @@ -151,6 +161,7 @@ if (!value && !showSuggestions) { textBoxHasFocus = false; showSuggestions = true; + listBoxHasFocus = true; currentLiIndex = options.length - 1; } @@ -184,7 +195,7 @@ const foundOption = getSelectedOption(selectedSuggestionItem); if (foundOption) { - value = foundOption.label; + value = ""; showSuggestions = false; handleSelectOption(foundOption); } @@ -204,6 +215,7 @@ */ showSuggestions = false; + listBoxHasFocus = false; textBoxHasFocus = true; break; @@ -216,6 +228,7 @@ */ case "ArrowDown": + listBoxHasFocus = true; currentLiIndex += 1; if (currentLiIndex === suggestionItems.length) { @@ -224,6 +237,7 @@ break; case "ArrowUp": + listBoxHasFocus = true; /** * Moves visual focus to the previous option. @@ -243,12 +257,14 @@ Moves visual focus to the textbox and moves the editing cursor one character to the right. */ + listBoxHasFocus = false; textBoxHasFocus = true; break; case "ArrowLeft": /** * Moves visual focus to the textbox and moves the editing cursor one character to the left. */ + listBoxHasFocus = false; textBoxHasFocus = true; break; default: @@ -290,7 +306,11 @@ aria-describedby={error ? `${name}-desc-error` : null} aria-activedescendant={`suggestion-item-${currentLiIndex}`} on:input={handleInput} - on:focus={() => (textBoxHasFocus = true)} + on:focus={handleInputFocused} + on:focusout={() => { + textBoxHasFocus = false; + showSuggestions = false; + }} />