Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Multiselect #17331

Merged
merged 20 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"vue-router": "^3.6.5",
"vue-rx": "^6.2.0",
"vue-virtual-scroll-list": "^2.3.5",
"vue2-teleport": "^1.0.1",
"vuedraggable": "^2.24.3",
"winbox": "^0.2.82",
"xml-beautifier": "^0.5.0"
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,15 @@
:elements-datatypes="item.elements_datatypes" />
<StatelessTags
v-if="!tagsDisabled || hasTags"
class="px-2 pb-2"
:value="tags"
:disabled="tagsDisabled"
:clickable="filterable"
:use-toggle-link="false"
@input="onTags"
@tag-click="onTagClick" />
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
<b-collapse :visible="expandDataset">
<b-collapse :visible="expandDataset" class="px-2 pb-2">
<DatasetDetails
v-if="expandDataset && item.id"
:id="item.id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function toggleHighlights() {
<template>
<div>
<div v-if="result && !isLoading" class="dataset">
<div class="p-2 details not-loading">
<div class="details not-loading">
<div class="summary">
<div v-if="stateText" class="mb-1">{{ stateText }}</div>
<div v-else-if="result.misc_blurb" class="blurb">
Expand Down
268 changes: 268 additions & 0 deletions client/src/components/TagsMultiselect/HeadlessMultiselect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import { nextTick } from "vue";

import HeadlessMultiselect from "./HeadlessMultiselect.vue";

describe("HeadlessMultiselect", () => {
// this function is not implemented in jsdom
// mocking it to avoid false errors
Element.prototype.scrollIntoView = jest.fn();

const localVue = getLocalVue();

type Props = InstanceType<typeof HeadlessMultiselect>["$props"];
const mountWithProps = (props: Props) => {
return mount(HeadlessMultiselect as any, {
propsData: props,
localVue,
attachTo: document.body,
});
};

const sampleOptions = ["#named", "#named_2", "#named_3", "abc", "def", "ghi"];

const selectors = {
openButton: ".toggle-button",
option: ".headless-multiselect__option",
highlighted: ".headless-multiselect__option.highlighted",
input: "fieldset input",
invalid: ".headless-multiselect__option.invalid",
} as const;

async function keyPress(wrapper: ReturnType<typeof mountWithProps>, key: string) {
wrapper.trigger("keydown", {
key,
code: key,
});
await nextTick();
wrapper.trigger("keyup", {
key,
code: key,
});
await nextTick();
}

async function open(wrapper: ReturnType<typeof mountWithProps>) {
wrapper.find(selectors.openButton).trigger("click");
await nextTick();
return wrapper.find(selectors.input);
}

async function close(wrapper: ReturnType<typeof mountWithProps>) {
await keyPress(wrapper.find(selectors.input), "Escape");
}

describe("while toggling the popup", () => {
it("shows and hides options", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

let options;

await open(wrapper);
options = wrapper.findAll(selectors.option);
expect(options.length).toBe(sampleOptions.length);

await close(wrapper);
options = wrapper.findAll(selectors.option);
expect(options.length).toBe(0);
});

it("retains focus", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

const input = await open(wrapper);
expect(input.element).toBe(document.activeElement);

await close(wrapper);
const button = wrapper.find(selectors.openButton);
expect(button.element).toBe(document.activeElement);
});
});

describe("while inputting text", () => {
it("filters options", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

let options;

const input = await open(wrapper);

await input.setValue("a");
options = wrapper.findAll(selectors.option);
expect(options.length).toBe(5);

await input.setValue("na");
options = wrapper.findAll(selectors.option);
expect(options.length).toBe(4);

await input.setValue("");
options = wrapper.findAll(selectors.option);
expect(options.length).toBe(6);
});

it("shows the search value on top", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

const input = await open(wrapper);

await input.setValue("bc");
const options = wrapper.findAll(selectors.option);

expect(options.at(0).find("span").text()).toBe("bc");
expect(options.at(1).find("span").text()).toBe("abc");
});

it("allows for switching the highlighted value", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

let highlighted;

const input = await open(wrapper);

highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("#named");

await keyPress(input, "ArrowDown");
highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("#named_2");

await keyPress(input, "ArrowDown");
highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("#named_3");

await keyPress(input, "ArrowUp");
highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("#named_2");
});

it("resets the highlighted option on input", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

let highlighted;

const input = await open(wrapper);

await keyPress(input, "ArrowDown");
highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("#named_2");

await input.setValue("a");

highlighted = wrapper.find(selectors.highlighted);
expect(highlighted.find("span").text()).toBe("a");
});

it("shows if the input value is valid", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
validator: (value: string) => value !== "invalid",
});

const input = await open(wrapper);
await input.setValue("valid");
expect(() => wrapper.get(selectors.invalid)).toThrow();

await input.setValue("invalid");
expect(() => wrapper.get(selectors.invalid)).not.toThrow();
});
});

describe("when selecting options", () => {
it("selects options via keyboard", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

const input = await open(wrapper);

await keyPress(input, "Enter");
expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named"]);

await keyPress(input, "ArrowDown");
await keyPress(input, "Enter");
expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named_2"]);
});

it("deselects options via keyboard", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: ["#named", "#named_2", "#named_3"],
});

const input = await open(wrapper);

await keyPress(input, "Enter");
expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named_2", "#named_3"]);

await keyPress(input, "ArrowDown");
await keyPress(input, "Enter");
expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named", "#named_3"]);
});

it("allows for adding new options", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

const input = await open(wrapper);
await input.setValue("123");
await keyPress(input, "Enter");

expect(wrapper.emitted()["addOption"]?.[0]?.[0]).toBe("123");
});

it("selects options with mouse", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: [] as string[],
});

await open(wrapper);
const options = wrapper.findAll(selectors.option);

await options.at(0).trigger("click");
expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named"]);

await options.at(1).trigger("click");
expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named_2"]);
});

it("deselects options with mouse", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
selected: ["#named", "#named_2", "#named_3"],
});

await open(wrapper);
const options = wrapper.findAll(selectors.option);

await options.at(0).trigger("click");
expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named_2", "#named_3"]);

await options.at(1).trigger("click");
expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named", "#named_3"]);
});
});
});
Loading
Loading