Skip to content

Commit

Permalink
Merge pull request galaxyproject#17331 from ElectronicBlueberry/tags-…
Browse files Browse the repository at this point in the history
…custom-select

Custom Multiselect
  • Loading branch information
mvdbeek authored Feb 12, 2024
2 parents 818c147 + 3ddd9f3 commit a3d7b50
Show file tree
Hide file tree
Showing 12 changed files with 787 additions and 233 deletions.
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

0 comments on commit a3d7b50

Please sign in to comment.