mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:23:17 -05:00
The `DMultiSelect` component provides a customizable multi-select dropdown with support for both mouse and keyboard interactions.  ### Parameters #### `@loadFn` (required) An async function that returns the data to populate the dropdown options. ```javascript const loadFn = async () => { return [ { id: 1, name: "Option 1" }, { id: 2, name: "Option 2" }, ]; }; ``` #### `@compareFn` A function used to determine equality between items. This is particularly useful when working with complex objects. By default, `id` will be used. ```javascript const compareFn = (a, b) => { return a.name === b.name; }; ``` #### `@selection` An array of pre-selected items that will be displayed as selected when the component renders. ```javascript const selection = [ { id: 1, name: "Option 1" }, { id: 2, name: "Option 2" }, ]; ``` #### `@label` Text label displayed in the trigger element when no items are selected. ```javascript @label="Select options" ``` ### Named Blocks #### :selection Block for customizing how selected items appear in the trigger. ```javascript <:selection as |result|>{{result.name}}</:selection> ``` #### :result Block for customizing how items appear in the dropdown list. ```javascript <:result as |result|>{{result.name}}</:result> ``` #### :result Block for customizing how errors appear in the component. ```javascript <:error as |error|>{{error}}</:error> ``` ### Example Usage ```javascript <DMultiSelect @loadFn={{this.loadOptions}} @selection={{this.selectedItems}} @compareFn={{this.compareItems}} @label="Select options"> <:selection as |result|>{{result.name}}</:selection> <:result as |result|>{{result.name}}</:result> <:error as |error|>{{error}}</:error> </DMultiSelect> ``` Co-Authored-By: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com> --------- Co-authored-by: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com>
223 lines
6.4 KiB
Plaintext
223 lines
6.4 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { action } from "@ember/object";
|
|
import {
|
|
click,
|
|
fillIn,
|
|
render,
|
|
triggerEvent,
|
|
triggerKeyEvent,
|
|
} from "@ember/test-helpers";
|
|
import { module, test } from "qunit";
|
|
import DMultiSelect from "discourse/components/d-multi-select";
|
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
|
|
class TestComponent extends Component {
|
|
@tracked selection = this.args.selection ?? [];
|
|
|
|
@action
|
|
onChange(newSelection) {
|
|
this.selection = newSelection;
|
|
}
|
|
|
|
@action
|
|
async loadFn(filter) {
|
|
return [
|
|
{ id: 1, name: "foo" },
|
|
{ id: 2, name: "bar" },
|
|
].filter((item) => {
|
|
return item.name.toLowerCase().includes(filter.toLowerCase());
|
|
});
|
|
}
|
|
|
|
<template>
|
|
<DMultiSelect
|
|
@loadFn={{if @loadFn @loadFn this.loadFn}}
|
|
@compareFn={{@compareFn}}
|
|
@onChange={{this.onChange}}
|
|
@selection={{this.selection}}
|
|
@label={{@label}}
|
|
>
|
|
<:selection as |result|>{{result.name}}</:selection>
|
|
<:result as |result|>{{result.name}}</:result>
|
|
<:error as |error|>{{error}}</:error>
|
|
</DMultiSelect>
|
|
</template>
|
|
}
|
|
|
|
module("Integration | Component | d-multi-select", function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
|
|
test("filter", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
|
|
await click(".d-multi-select-trigger");
|
|
await fillIn(".d-multi-select__search-input", "bar");
|
|
|
|
assert.dom(".d-multi-select__result:nth-child(1)").hasText("bar");
|
|
assert.dom(".d-multi-select__result:nth-child(2)").doesNotExist();
|
|
});
|
|
|
|
test("@selection", async function (assert) {
|
|
const selection = [{ id: 1, name: "foo" }];
|
|
|
|
await render(
|
|
<template><TestComponent @selection={{selection}} /></template>
|
|
);
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("foo");
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
|
.doesNotExist();
|
|
});
|
|
|
|
test("@onChange", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
await click(".d-multi-select__result:nth-child(1)");
|
|
await click(".d-multi-select__result:nth-child(2)");
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("foo");
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
|
.hasText("bar");
|
|
});
|
|
|
|
test("keyboard", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
|
|
|
|
assert
|
|
.dom(".d-multi-select__result:nth-child(1)")
|
|
.hasClass("--preselected");
|
|
|
|
await triggerKeyEvent(document.activeElement, "keydown", "ArrowUp");
|
|
|
|
assert
|
|
.dom(".d-multi-select__result:nth-child(1)")
|
|
.hasClass("--preselected");
|
|
|
|
await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
|
|
|
|
assert
|
|
.dom(".d-multi-select__result:nth-child(2)")
|
|
.hasClass("--preselected");
|
|
|
|
await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
|
|
|
|
assert
|
|
.dom(".d-multi-select__result:nth-child(2)")
|
|
.hasClass("--preselected");
|
|
|
|
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("bar");
|
|
});
|
|
|
|
test("@compareFn", async function (assert) {
|
|
const compareFn = (a, b) => {
|
|
return a.name === b.name;
|
|
};
|
|
|
|
const loadFn = async () => {
|
|
return [{ name: "foo" }, { name: "bar" }];
|
|
};
|
|
|
|
await render(
|
|
<template>
|
|
<TestComponent @compareFn={{compareFn}} @loadFn={{loadFn}} />
|
|
</template>
|
|
);
|
|
|
|
await click(".d-multi-select-trigger");
|
|
await click(".d-multi-select__result:nth-child(1)");
|
|
await click(".d-multi-select__result:nth-child(2)");
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("foo");
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
|
.hasText("bar");
|
|
});
|
|
|
|
test("@label", async function (assert) {
|
|
await render(<template><TestComponent @label="label" /></template>);
|
|
|
|
assert.dom(".d-multi-select-trigger__label").hasText("label");
|
|
});
|
|
|
|
test("@loadFn", async function (assert) {
|
|
const loadFn = async () => {
|
|
return [
|
|
{ id: 1, name: "cat" },
|
|
{ id: 2, name: "dog" },
|
|
];
|
|
};
|
|
|
|
await render(<template><TestComponent @loadFn={{loadFn}} /></template>);
|
|
|
|
await click(".d-multi-select-trigger");
|
|
|
|
assert.dom(".d-multi-select__result:nth-child(1)").hasText("cat");
|
|
assert.dom(".d-multi-select__result:nth-child(2)").hasText("dog");
|
|
});
|
|
|
|
test("select item", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
await click(".d-multi-select__result:nth-child(1)");
|
|
await click(".d-multi-select__result:nth-child(2)");
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("foo");
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
|
.hasText("bar");
|
|
});
|
|
|
|
test("unselect item", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
await click(".d-multi-select__result:nth-child(1)");
|
|
await click(".d-multi-select__result:nth-child(2)");
|
|
await click(".d-multi-select-trigger__selected-item:nth-child(1)");
|
|
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
|
.hasText("bar");
|
|
assert
|
|
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
|
.doesNotExist();
|
|
});
|
|
|
|
test("preselect item", async function (assert) {
|
|
await render(<template><TestComponent /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
await triggerEvent(".d-multi-select__result:nth-child(1)", "mouseenter");
|
|
|
|
assert
|
|
.dom(".d-multi-select__result:nth-child(1)")
|
|
.hasClass("--preselected");
|
|
});
|
|
|
|
test(":error", async function (assert) {
|
|
const loadFn = async () => {
|
|
throw new Error("error");
|
|
};
|
|
|
|
await render(<template><TestComponent @loadFn={{loadFn}} /></template>);
|
|
await click(".d-multi-select-trigger");
|
|
|
|
assert.dom(".d-multi-select__error").hasText("Error: error");
|
|
});
|
|
});
|