From f5e8e737ad3fd7939f2c102d9dcbc5948294e0bf Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Tue, 25 Jul 2023 11:00:02 -0400 Subject: [PATCH] UX: Compact option for multi-selects (#22239) Adds an alternative to the default multi select item, better suited for quickly adding/removing tags. --- .../discourse/app/templates/topic.hbs | 2 + .../select-kit/mini-tag-chooser-test.js | 63 ++++++++++++++++++- .../addon/components/mini-tag-chooser.js | 1 + .../addon/components/multi-select.hbs | 34 +++++----- .../addon/components/multi-select.js | 1 + .../multi-select/multi-select-header.hbs | 30 +++++++-- .../select-kit/addon/components/select-kit.js | 8 +++ .../select-kit/select-kit-filter.js | 9 +++ .../select-kit/select-kit-header.js | 12 +++- app/assets/stylesheets/common/base/topic.scss | 6 +- .../common/select-kit/multi-select.scss | 36 +++++++++++ config/locales/client.en.yml | 4 +- .../components/sections/atoms/dropdowns.hbs | 22 +++++++ .../javascripts/discourse/lib/dummy-data.js | 2 + 14 files changed, 204 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs index ec1ddb47d5e..e9aac74b672 100644 --- a/app/assets/javascripts/discourse/app/templates/topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/topic.hbs @@ -72,6 +72,8 @@ filterable=true categoryId=this.buffered.category_id minimum=this.minimumRequiredTags + filterPlaceholder="topic_edit.tag_filter_placeholder" + useHeaderFilter=true }} /> {{/if}} diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js index 4824aea118a..876ed6b859a 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js @@ -1,6 +1,6 @@ import { module, test } from "qunit"; 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 I18n from "I18n"; 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`` + ); + + 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`` + ); + + 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"); + }); + } +); diff --git a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js index d5e1263a2e5..27d510ab741 100644 --- a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js @@ -26,6 +26,7 @@ export default MultiSelectComponent.extend(TagsMixin, { closeOnChange: false, maximum: "maxTagsPerTopic", autoInsertNoneItem: false, + useHeaderFilter: false, }, modifyComponentForRow(collection, item) { diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select.hbs b/app/assets/javascripts/select-kit/addon/components/multi-select.hbs index bff74779a03..2e1aa9af9cf 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select.hbs +++ b/app/assets/javascripts/select-kit/addon/components/multi-select.hbs @@ -12,23 +12,25 @@ @selectKit={{this.selectKit}} @id={{concat this.selectKit.uniqueID "-body"}} > - {{component - this.selectKit.options.filterComponent - selectKit=this.selectKit - id=(concat this.selectKit.uniqueID "-filter") - }} + {{#unless this.selectKit.options.useHeaderFilter}} + {{component + this.selectKit.options.filterComponent + selectKit=this.selectKit + id=(concat this.selectKit.uniqueID "-filter") + }} - {{#if this.selectedContent.length}} -
- {{#each this.selectedContent as |item|}} - {{component - this.selectKit.options.selectedChoiceComponent - item=item - selectKit=this.selectKit - }} - {{/each}} -
- {{/if}} + {{#if this.selectedContent.length}} +
+ {{#each this.selectedContent as |item|}} + {{component + this.selectKit.options.selectedChoiceComponent + item=item + selectKit=this.selectKit + }} + {{/each}} +
+ {{/if}} + {{/unless}} {{#each this.collections as |collection|}} {{component diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select.js b/app/assets/javascripts/select-kit/addon/components/multi-select.js index e15d96d9fe7..7ab70bb3c12 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select.js +++ b/app/assets/javascripts/select-kit/addon/components/multi-select.js @@ -21,6 +21,7 @@ export default SelectKitComponent.extend({ autoFilterable: true, caretDownIcon: "caretIcon", caretUpIcon: "caretIcon", + useHeaderFilter: false, }, caretIcon: computed("value.[]", function () { diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.hbs b/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.hbs index 9d7c0ce4d64..d6ffc820e8c 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.hbs +++ b/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.hbs @@ -3,10 +3,30 @@ {{d-icon icon}} {{/each}} - + {{#if this.selectKit.options.useHeaderFilter}} +
+ {{#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") + }} +
+ {{else}} + + + {{d-icon this.caretIcon class="caret-icon"}} + {{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit.js b/app/assets/javascripts/select-kit/addon/components/select-kit.js index b3aa9acefb7..a2c617320bb 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -114,6 +114,7 @@ export default Component.extend( highlightPrevious: bind(this, this._highlightPrevious), highlightLast: bind(this, this._highlightLast), highlightFirst: bind(this, this._highlightFirst), + deselectLast: bind(this, this._deselectLast), change: bind(this, this._onChangeWrapper), select: bind(this, this.select), deselect: bind(this, this.deselect), @@ -295,6 +296,7 @@ export default Component.extend( minimum: null, autoInsertNoneItem: true, closeOnChange: true, + useHeaderFilter: false, limitMatches: null, placement: isDocumentRTL() ? "bottom-end" : "bottom-start", 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) { if (!isPresent(value)) { this._onClearSelection(); diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-filter.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-filter.js index 47c10fdec3b..9c7efdb5ba7 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-filter.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-filter.js @@ -79,6 +79,12 @@ export default Component.extend(UtilsMixin, { return true; } + if (event.key === "Backspace" && !this.selectKit.filter) { + this.selectKit.deselectLast(); + event.preventDefault(); + return false; + } + if (event.key === "ArrowUp") { this.selectKit.highlightLast(); event.preventDefault(); @@ -86,6 +92,9 @@ export default Component.extend(UtilsMixin, { } if (event.key === "ArrowDown") { + if (!this.selectKit.isExpanded) { + this.selectKit.open(event); + } this.selectKit.highlightFirst(); event.preventDefault(); return false; diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js index 5bd78815f7b..09c322e4254 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js @@ -64,6 +64,12 @@ export default Component.extend(UtilsMixin, { event.preventDefault(); event.stopPropagation(); + if ( + event.target?.classList.contains("selected-choice") || + event.target.parentNode?.classList.contains("selected-choice") + ) { + return false; + } this.selectKit.toggle(event); }, @@ -74,7 +80,11 @@ export default Component.extend(UtilsMixin, { }, keyDown(event) { - if (this.selectKit.isDisabled || this.selectKit.options.disabled) { + if ( + this.selectKit.isDisabled || + this.selectKit.options.disabled || + this.selectKit.options.useHeaderFilter + ) { return; } diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 09cf6aa3560..46dbe81f9b2 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -301,14 +301,16 @@ a.badge-category { } .category-chooser, .mini-tag-chooser { - flex: 1 1 49%; + flex: 1 1 35%; margin: 0 0 9px 0; @media all and (max-width: 500px) { flex: 1 1 100%; } } .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) { margin-left: 0; } diff --git a/app/assets/stylesheets/common/select-kit/multi-select.scss b/app/assets/stylesheets/common/select-kit/multi-select.scss index 4f1ad6f4990..c96ba9f386d 100644 --- a/app/assets/stylesheets/common/select-kit/multi-select.scss +++ b/app/assets/stylesheets/common/select-kit/multi-select.scss @@ -55,6 +55,42 @@ @include ellipsis; 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, diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fffdee4ec82..baf90338b33 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2044,7 +2044,7 @@ en: summarized_on: "Summarized with AI on %{date}" model_used: "AI used: %{model}" outdated: "Summary is outdated" - outdated_posts: + outdated_posts: one: "(%{count} post missing)" other: "(%{count} posts missing)" 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: title: "This topic is a personal message" help: "This topic is a personal message" + topic_edit: + tag_filter_placeholder: "+ tag" posts: "Posts" pending_posts: label: "Pending" diff --git a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/dropdowns.hbs b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/dropdowns.hbs index 562582bc536..4167556bfa8 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/dropdowns.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/dropdowns.hbs @@ -123,6 +123,28 @@ + +
+ +
+
+ + +
+ +
+
+