UX: Compact option for multi-selects (#22239)
Adds an alternative to the default multi select item, better suited for quickly adding/removing tags.
This commit is contained in:
parent
4e5756e3ae
commit
f5e8e737ad
|
@ -72,6 +72,8 @@
|
||||||
filterable=true
|
filterable=true
|
||||||
categoryId=this.buffered.category_id
|
categoryId=this.buffered.category_id
|
||||||
minimum=this.minimumRequiredTags
|
minimum=this.minimumRequiredTags
|
||||||
|
filterPlaceholder="topic_edit.tag_filter_placeholder"
|
||||||
|
useHeaderFilter=true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
import { render } from "@ember/test-helpers";
|
import { click, render, triggerKeyEvent } from "@ember/test-helpers";
|
||||||
import { exists, query, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
import { exists, query, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
|
@ -148,3 +148,64 @@ module(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
module(
|
||||||
|
"Integration | Component | select-kit/mini-tag-chooser useHeaderFilter=true",
|
||||||
|
function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.set("subject", selectKit());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("displays tags and filter in header", async function (assert) {
|
||||||
|
this.set("value", ["apple", "orange", "potato"]);
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<MiniTagChooser @value={{this.value}} @options={{hash filterable=true useHeaderFilter=true}} />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(this.subject.header().value(), "apple,orange,potato");
|
||||||
|
|
||||||
|
assert.dom(".select-kit-header--filter").exists();
|
||||||
|
assert.dom(".select-kit-header button[data-name='apple']").exists();
|
||||||
|
assert.dom(".select-kit-header button[data-name='orange']").exists();
|
||||||
|
assert.dom(".select-kit-header button[data-name='potato']").exists();
|
||||||
|
|
||||||
|
const filterInput = ".select-kit-header .filter-input";
|
||||||
|
await click(filterInput);
|
||||||
|
|
||||||
|
await triggerKeyEvent(filterInput, "keydown", "ArrowDown");
|
||||||
|
await triggerKeyEvent(filterInput, "keydown", "Enter");
|
||||||
|
|
||||||
|
assert.dom(".select-kit-header button[data-name='monkey']").exists();
|
||||||
|
|
||||||
|
await triggerKeyEvent(filterInput, "keydown", "Backspace");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".select-kit-header button[data-name='monkey']")
|
||||||
|
.doesNotExist();
|
||||||
|
|
||||||
|
await this.subject.fillInFilter("foo");
|
||||||
|
await triggerKeyEvent(filterInput, "keydown", "Backspace");
|
||||||
|
|
||||||
|
assert.dom(".select-kit-header button[data-name='potato']").exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removing a tag does not display the dropdown", async function (assert) {
|
||||||
|
this.set("value", ["apple", "orange", "potato"]);
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<MiniTagChooser @value={{this.value}} @options={{hash filterable=true useHeaderFilter=true}} />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(this.subject.header().value(), "apple,orange,potato");
|
||||||
|
|
||||||
|
await click(".select-kit-header button[data-name='apple']");
|
||||||
|
|
||||||
|
assert.dom(".select-kit-collection").doesNotExist();
|
||||||
|
assert.dom(".select-kit-header button[data-name='apple']").doesNotExist();
|
||||||
|
assert.strictEqual(this.subject.header().value(), "orange,potato");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default MultiSelectComponent.extend(TagsMixin, {
|
||||||
closeOnChange: false,
|
closeOnChange: false,
|
||||||
maximum: "maxTagsPerTopic",
|
maximum: "maxTagsPerTopic",
|
||||||
autoInsertNoneItem: false,
|
autoInsertNoneItem: false,
|
||||||
|
useHeaderFilter: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
modifyComponentForRow(collection, item) {
|
modifyComponentForRow(collection, item) {
|
||||||
|
|
|
@ -12,23 +12,25 @@
|
||||||
@selectKit={{this.selectKit}}
|
@selectKit={{this.selectKit}}
|
||||||
@id={{concat this.selectKit.uniqueID "-body"}}
|
@id={{concat this.selectKit.uniqueID "-body"}}
|
||||||
>
|
>
|
||||||
{{component
|
{{#unless this.selectKit.options.useHeaderFilter}}
|
||||||
this.selectKit.options.filterComponent
|
{{component
|
||||||
selectKit=this.selectKit
|
this.selectKit.options.filterComponent
|
||||||
id=(concat this.selectKit.uniqueID "-filter")
|
selectKit=this.selectKit
|
||||||
}}
|
id=(concat this.selectKit.uniqueID "-filter")
|
||||||
|
}}
|
||||||
|
|
||||||
{{#if this.selectedContent.length}}
|
{{#if this.selectedContent.length}}
|
||||||
<div class="selected-content">
|
<div class="selected-content">
|
||||||
{{#each this.selectedContent as |item|}}
|
{{#each this.selectedContent as |item|}}
|
||||||
{{component
|
{{component
|
||||||
this.selectKit.options.selectedChoiceComponent
|
this.selectKit.options.selectedChoiceComponent
|
||||||
item=item
|
item=item
|
||||||
selectKit=this.selectKit
|
selectKit=this.selectKit
|
||||||
}}
|
}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
{{#each this.collections as |collection|}}
|
{{#each this.collections as |collection|}}
|
||||||
{{component
|
{{component
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default SelectKitComponent.extend({
|
||||||
autoFilterable: true,
|
autoFilterable: true,
|
||||||
caretDownIcon: "caretIcon",
|
caretDownIcon: "caretIcon",
|
||||||
caretUpIcon: "caretIcon",
|
caretUpIcon: "caretIcon",
|
||||||
|
useHeaderFilter: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
caretIcon: computed("value.[]", function () {
|
caretIcon: computed("value.[]", function () {
|
||||||
|
|
|
@ -3,10 +3,30 @@
|
||||||
{{d-icon icon}}
|
{{d-icon icon}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
<MultiSelect::FormatSelectedContent
|
{{#if this.selectKit.options.useHeaderFilter}}
|
||||||
@content={{or this.selectedContent this.selectKit.noneItem}}
|
<div class="select-kit-header--filter">
|
||||||
@selectKit={{this.selectKit}}
|
{{#if this.selectedContent.length}}
|
||||||
/>
|
{{#each this.selectedContent as |item|}}
|
||||||
|
{{component
|
||||||
|
this.selectKit.options.selectedChoiceComponent
|
||||||
|
item=item
|
||||||
|
selectKit=this.selectKit
|
||||||
|
}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{d-icon this.caretIcon class="caret-icon"}}
|
{{component
|
||||||
|
this.selectKit.options.filterComponent
|
||||||
|
selectKit=this.selectKit
|
||||||
|
id=(concat this.selectKit.uniqueID "-filter")
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<MultiSelect::FormatSelectedContent
|
||||||
|
@content={{or this.selectedContent this.selectKit.noneItem}}
|
||||||
|
@selectKit={{this.selectKit}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{d-icon this.caretIcon class="caret-icon"}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
|
@ -114,6 +114,7 @@ export default Component.extend(
|
||||||
highlightPrevious: bind(this, this._highlightPrevious),
|
highlightPrevious: bind(this, this._highlightPrevious),
|
||||||
highlightLast: bind(this, this._highlightLast),
|
highlightLast: bind(this, this._highlightLast),
|
||||||
highlightFirst: bind(this, this._highlightFirst),
|
highlightFirst: bind(this, this._highlightFirst),
|
||||||
|
deselectLast: bind(this, this._deselectLast),
|
||||||
change: bind(this, this._onChangeWrapper),
|
change: bind(this, this._onChangeWrapper),
|
||||||
select: bind(this, this.select),
|
select: bind(this, this.select),
|
||||||
deselect: bind(this, this.deselect),
|
deselect: bind(this, this.deselect),
|
||||||
|
@ -295,6 +296,7 @@ export default Component.extend(
|
||||||
minimum: null,
|
minimum: null,
|
||||||
autoInsertNoneItem: true,
|
autoInsertNoneItem: true,
|
||||||
closeOnChange: true,
|
closeOnChange: true,
|
||||||
|
useHeaderFilter: false,
|
||||||
limitMatches: null,
|
limitMatches: null,
|
||||||
placement: isDocumentRTL() ? "bottom-end" : "bottom-start",
|
placement: isDocumentRTL() ? "bottom-end" : "bottom-start",
|
||||||
verticalOffset: 3,
|
verticalOffset: 3,
|
||||||
|
@ -801,6 +803,12 @@ export default Component.extend(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_deselectLast() {
|
||||||
|
if (this.selectKit.hasSelection) {
|
||||||
|
this.deselectByValue(this.value[this.value.length - 1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
select(value, item) {
|
select(value, item) {
|
||||||
if (!isPresent(value)) {
|
if (!isPresent(value)) {
|
||||||
this._onClearSelection();
|
this._onClearSelection();
|
||||||
|
|
|
@ -79,6 +79,12 @@ export default Component.extend(UtilsMixin, {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === "Backspace" && !this.selectKit.filter) {
|
||||||
|
this.selectKit.deselectLast();
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "ArrowUp") {
|
if (event.key === "ArrowUp") {
|
||||||
this.selectKit.highlightLast();
|
this.selectKit.highlightLast();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -86,6 +92,9 @@ export default Component.extend(UtilsMixin, {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "ArrowDown") {
|
if (event.key === "ArrowDown") {
|
||||||
|
if (!this.selectKit.isExpanded) {
|
||||||
|
this.selectKit.open(event);
|
||||||
|
}
|
||||||
this.selectKit.highlightFirst();
|
this.selectKit.highlightFirst();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -64,6 +64,12 @@ export default Component.extend(UtilsMixin, {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.target?.classList.contains("selected-choice") ||
|
||||||
|
event.target.parentNode?.classList.contains("selected-choice")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.selectKit.toggle(event);
|
this.selectKit.toggle(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -74,7 +80,11 @@ export default Component.extend(UtilsMixin, {
|
||||||
},
|
},
|
||||||
|
|
||||||
keyDown(event) {
|
keyDown(event) {
|
||||||
if (this.selectKit.isDisabled || this.selectKit.options.disabled) {
|
if (
|
||||||
|
this.selectKit.isDisabled ||
|
||||||
|
this.selectKit.options.disabled ||
|
||||||
|
this.selectKit.options.useHeaderFilter
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -301,14 +301,16 @@ a.badge-category {
|
||||||
}
|
}
|
||||||
.category-chooser,
|
.category-chooser,
|
||||||
.mini-tag-chooser {
|
.mini-tag-chooser {
|
||||||
flex: 1 1 49%;
|
flex: 1 1 35%;
|
||||||
margin: 0 0 9px 0;
|
margin: 0 0 9px 0;
|
||||||
@media all and (max-width: 500px) {
|
@media all and (max-width: 500px) {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mini-tag-chooser {
|
.mini-tag-chooser {
|
||||||
margin-left: 2%; // category/tag chooser are 49% wide, so this is 1% * 2
|
flex: 1 1 54%;
|
||||||
|
margin: 0 0 9px 0;
|
||||||
|
margin-left: 1%; // category at 40%, tag chooser at 58%
|
||||||
@media all and (max-width: 500px) {
|
@media all and (max-width: 500px) {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,42 @@
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-kit-header--filter {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: -0.25em;
|
||||||
|
margin-bottom: -0.45em;
|
||||||
|
position: relative;
|
||||||
|
.selected-choice {
|
||||||
|
margin: 0 0.25em 0.25em 0;
|
||||||
|
padding: 0.2em 0.3em;
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
|
||||||
|
&.selected-choice-color {
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-kit-filter {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 1 1 30px;
|
||||||
|
width: auto;
|
||||||
|
margin-left: 0.25em;
|
||||||
|
position: static;
|
||||||
|
&.is-expanded {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.filter-input {
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
min-height: 28px;
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-expanded .multi-select-header,
|
&.is-expanded .multi-select-header,
|
||||||
|
|
|
@ -2044,7 +2044,7 @@ en:
|
||||||
summarized_on: "Summarized with AI on %{date}"
|
summarized_on: "Summarized with AI on %{date}"
|
||||||
model_used: "AI used: %{model}"
|
model_used: "AI used: %{model}"
|
||||||
outdated: "Summary is outdated"
|
outdated: "Summary is outdated"
|
||||||
outdated_posts:
|
outdated_posts:
|
||||||
one: "(%{count} post missing)"
|
one: "(%{count} post missing)"
|
||||||
other: "(%{count} posts missing)"
|
other: "(%{count} posts missing)"
|
||||||
enabled_description: "You're viewing this topic top replies: the most interesting posts as determined by the community."
|
enabled_description: "You're viewing this topic top replies: the most interesting posts as determined by the community."
|
||||||
|
@ -3900,6 +3900,8 @@ en:
|
||||||
personal_message:
|
personal_message:
|
||||||
title: "This topic is a personal message"
|
title: "This topic is a personal message"
|
||||||
help: "This topic is a personal message"
|
help: "This topic is a personal message"
|
||||||
|
topic_edit:
|
||||||
|
tag_filter_placeholder: "+ tag"
|
||||||
posts: "Posts"
|
posts: "Posts"
|
||||||
pending_posts:
|
pending_posts:
|
||||||
label: "Pending"
|
label: "Pending"
|
||||||
|
|
|
@ -123,6 +123,28 @@
|
||||||
<MultiSelect @content={{@dummy.options}} @onChange={{@dummyAction}} />
|
<MultiSelect @content={{@dummy.options}} @onChange={{@dummyAction}} />
|
||||||
</StyleguideExample>
|
</StyleguideExample>
|
||||||
|
|
||||||
|
<StyleguideExample @title="<MiniTagChooser>">
|
||||||
|
<div class="inline-form">
|
||||||
|
<MiniTagChooser
|
||||||
|
@value={{@dummy.selectedTags}}
|
||||||
|
@options={{hash filterable=true}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyleguideExample>
|
||||||
|
|
||||||
|
<StyleguideExample @title="<MiniTagChooser> with useHeaderFilter=true">
|
||||||
|
<div class="inline-form">
|
||||||
|
<MiniTagChooser
|
||||||
|
@value={{@dummy.selectedTags}}
|
||||||
|
@options={{hash
|
||||||
|
filterable=true
|
||||||
|
filterPlaceholder="topic_edit.tag_filter_placeholder"
|
||||||
|
useHeaderFilter=true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyleguideExample>
|
||||||
|
|
||||||
<StyleguideExample @title="admin <GroupChooser>">
|
<StyleguideExample @title="admin <GroupChooser>">
|
||||||
<GroupChooser
|
<GroupChooser
|
||||||
@selected={{@dummy.selectedGroups}}
|
@selected={{@dummy.selectedGroups}}
|
||||||
|
|
|
@ -282,6 +282,8 @@ export function createData(store) {
|
||||||
colors: "f49|c89|564897",
|
colors: "f49|c89|564897",
|
||||||
|
|
||||||
charCounterContent: "",
|
charCounterContent: "",
|
||||||
|
|
||||||
|
selectedTags: ["apple", "orange", "potato"],
|
||||||
};
|
};
|
||||||
|
|
||||||
return _data;
|
return _data;
|
||||||
|
|
Loading…
Reference in New Issue