diff --git a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs index 9e17bda2ac3..bac1facc694 100644 --- a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs @@ -3,7 +3,9 @@ valueProperty="value" content=groupOptions value=groupId - allowAny=filter.allow_any none="admin.dashboard.reports.groups" onChange=(action "onChange") + options=(hash + allowAny=filter.allow_any + ) }} diff --git a/app/assets/javascripts/admin/addon/templates/components/site-settings/compact-list.hbs b/app/assets/javascripts/admin/addon/templates/components/site-settings/compact-list.hbs index 4d37e15e871..c896d5ecc35 100644 --- a/app/assets/javascripts/admin/addon/templates/components/site-settings/compact-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/site-settings/compact-list.hbs @@ -1,10 +1,12 @@ {{list-setting value=settingValue settingName=setting.setting - allowAny=allowAny choices=settingChoices onChange=(action "onChangeListSetting") onChangeChoices=(action "onChangeChoices") + options=(hash + allowAny=allowAny + ) }} {{setting-validation-message message=validationMessage}} diff --git a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs index 853daac745f..94b1d12325f 100644 --- a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs @@ -21,7 +21,9 @@ {{/if}} {{combo-box - allowAny=true + options=(hash + allowAny=true + ) none=noneKey valueProperty=null nameProperty=null diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 7ffdb991eeb..c319408bbc3 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -682,6 +682,7 @@ export default Component.extend(ComposerUpload, { extraButtons(toolbar) { toolbar.addButton({ + tabindex: "0", id: "quote", group: "fontStyles", icon: "far-comment", diff --git a/app/assets/javascripts/discourse/app/components/composer-save-button.js b/app/assets/javascripts/discourse/app/components/composer-save-button.js index 4c7500d6402..efa5d2bd773 100644 --- a/app/assets/javascripts/discourse/app/components/composer-save-button.js +++ b/app/assets/javascripts/discourse/app/components/composer-save-button.js @@ -1,7 +1,6 @@ import Button from "discourse/components/d-button"; export default Button.extend({ - tabindex: 5, classNameBindings: [":btn-primary", ":create", "disableSubmit:disabled"], title: "composer.title", }); diff --git a/app/assets/javascripts/discourse/app/components/composer-user-selector.js b/app/assets/javascripts/discourse/app/components/composer-user-selector.js index fd7babb76d0..8bf2d053912 100644 --- a/app/assets/javascripts/discourse/app/components/composer-user-selector.js +++ b/app/assets/javascripts/discourse/app/components/composer-user-selector.js @@ -1,6 +1,5 @@ import Component from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; -import putCursorAtEnd from "discourse/lib/put-cursor-at-end"; export default Component.extend({ init() { @@ -12,7 +11,7 @@ export default Component.extend({ this._super(...arguments); if (this.focusTarget === "usernames") { - putCursorAtEnd(this.element.querySelector("input")); + this.element.querySelector(".select-kit .select-kit-header").focus(); } }, diff --git a/app/assets/javascripts/discourse/app/components/d-button.js b/app/assets/javascripts/discourse/app/components/d-button.js index 36987bebb3d..73d635009e9 100644 --- a/app/assets/javascripts/discourse/app/components/d-button.js +++ b/app/assets/javascripts/discourse/app/components/d-button.js @@ -21,6 +21,7 @@ export default Component.extend({ translatedAriaLabel: null, forwardEvent: false, preventFocus: false, + onKeyDown: null, isLoading: computed({ set(key, value) { @@ -105,6 +106,13 @@ export default Component.extend({ } }, + keyDown(e) { + if (this.onKeyDown) { + e.stopPropagation(); + this.onKeyDown(e); + } + }, + click(event) { let { action } = this; @@ -132,6 +140,7 @@ export default Component.extend({ DiscourseURL.routeTo(this.href); } + event.preventDefault(); event.stopPropagation(); return false; diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index e2cc964cdbe..c6ebb29af00 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -37,6 +37,7 @@ import { siteDir } from "discourse/lib/text-direction"; import toMarkdown from "discourse/lib/to-markdown"; import { translations } from "pretty-text/emoji/data"; import { wantsNewWindow } from "discourse/lib/intercept-click"; +import { action } from "@ember/object"; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -182,6 +183,7 @@ class Toolbar { const createdButton = { id: button.id, + tabindex: button.tabindex || "-1", className: button.className || button.id, label: button.label, icon: button.label ? null : button.icon || button.id, @@ -442,13 +444,19 @@ export default Component.extend({ if (this._state !== "inDOM" || !this.element) { return; } - const $preview = $(this.element.querySelector(".d-editor-preview")); - if ($preview.length === 0) { + + const preview = this.element.querySelector(".d-editor-preview"); + if (!preview) { return; } + // prevents any tab focus in preview + preview.querySelectorAll("a").forEach((anchor) => { + anchor.setAttribute("tabindex", "-1"); + }); + if (this.previewUpdated) { - this.previewUpdated($preview); + this.previewUpdated($(preview)); } }); }); @@ -1027,6 +1035,45 @@ export default Component.extend({ }); }, + @action + rovingButtonBar(event) { + let target = event.target; + let siblingFinder; + if (event.code === "ArrowRight") { + siblingFinder = "nextElementSibling"; + } else if (event.code === "ArrowLeft") { + siblingFinder = "previousElementSibling"; + } else { + return true; + } + + while ( + target.parentNode && + !target.parentNode.classList.contains("d-editor-button-bar") + ) { + target = target.parentNode; + } + + let focusable = target[siblingFinder]; + if (focusable) { + while ( + (focusable.tagName !== "BUTTON" && + !focusable.classList.contains("select-kit")) || + focusable.classList.contains("hidden") + ) { + focusable = focusable[siblingFinder]; + } + + if (focusable?.tagName === "DETAILS") { + focusable = focusable.querySelector("summary"); + } + + focusable?.focus(); + } + + return true; + }, + actions: { emoji() { if (this.disabled) { diff --git a/app/assets/javascripts/discourse/app/components/d-modal-body.js b/app/assets/javascripts/discourse/app/components/d-modal-body.js index 4f2f38292fc..2ea7d11324d 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal-body.js +++ b/app/assets/javascripts/discourse/app/components/d-modal-body.js @@ -5,7 +5,6 @@ export default Component.extend({ fixed: false, submitOnEnter: true, dismissable: true, - autoFocus: true, didInsertElement() { this._super(...arguments); @@ -35,10 +34,8 @@ export default Component.extend({ const maxHeightFloat = parseFloat(maxHeight) / 100.0; if (maxHeightFloat > 0) { const viewPortHeight = $(window).height(); - $(this.element).css( - "max-height", - Math.floor(maxHeightFloat * viewPortHeight) + "px" - ); + this.element.style.maxHeight = + Math.floor(maxHeightFloat * viewPortHeight) + "px"; } } @@ -52,8 +49,7 @@ export default Component.extend({ "rawSubtitle", "submitOnEnter", "dismissable", - "headerClass", - "autoFocus" + "headerClass" ) ); }, diff --git a/app/assets/javascripts/discourse/app/components/d-modal.js b/app/assets/javascripts/discourse/app/components/d-modal.js index 652035b4675..a08ec543e95 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal.js +++ b/app/assets/javascripts/discourse/app/components/d-modal.js @@ -1,9 +1,8 @@ import { computed } from "@ember/object"; import Component from "@ember/component"; import I18n from "I18n"; -import afterTransition from "discourse/lib/after-transition"; -import { next } from "@ember/runloop"; -import { on } from "discourse-common/utils/decorators"; +import { next, schedule } from "@ember/runloop"; +import { bind, on } from "discourse-common/utils/decorators"; export default Component.extend({ classNameBindings: [ @@ -48,26 +47,20 @@ export default Component.extend({ @on("didInsertElement") setUp() { - $("html").on("keyup.discourse-modal", (e) => { - // only respond to events when the modal is visible - if (!this.element.classList.contains("hidden")) { - if (e.which === 27 && this.dismissable) { - next(() => this.attrs.closeModal("initiatedByESC")); - } - - if (e.which === 13 && this.triggerClickOnEnter(e)) { - next(() => $(".modal-footer .btn-primary").click()); - } - } - }); - this.appEvents.on("modal:body-shown", this, "_modalBodyShown"); + document.documentElement.addEventListener( + "keydown", + this._handleModalEvents + ); }, @on("willDestroyElement") cleanUp() { - $("html").off("keyup.discourse-modal"); this.appEvents.off("modal:body-shown", this, "_modalBodyShown"); + document.documentElement.removeEventListener( + "keydown", + this._handleModalEvents + ); }, triggerClickOnEnter(e) { @@ -141,22 +134,75 @@ export default Component.extend({ this.set("headerClass", data.headerClass || null); - if (this.element && data.autoFocus) { - let focusTarget = this.element.querySelector( - ".modal-body input[autofocus]" - ); + schedule("afterRender", () => { + this._trapTab(); + }); + }, - if (!focusTarget && !this.site.mobileView) { - focusTarget = this.element.querySelector( - ".modal-body input, .modal-body button, .modal-footer input, .modal-footer button" - ); + @bind + _handleModalEvents(event) { + if (this.element.classList.contains("hidden")) { + return; + } - if (!focusTarget) { - focusTarget = this.element.querySelector(".modal-header button"); - } + if (event.key === "Escape" && this.dismissable) { + next(() => this.attrs.closeModal("initiatedByESC")); + } + if (event.key === "Enter" && this.triggerClickOnEnter(event)) { + this.element?.querySelector(".modal-footer .btn-primary")?.click(); + } + if (event.key === "Tab") { + this._trapTab(event); + } + }, + + _trapTab(event) { + if (this.element.classList.contains("hidden")) { + return true; + } + + const innerContainer = this.element.querySelector(".modal-inner-container"); + if (!innerContainer) { + return; + } + + let focusableElements = + '[autofocus], a, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])'; + + if (!event) { + // on first trap we don't allow to focus modal-close + // and apply manual focus only if we don't have any autofocus element + const autofocusedElement = innerContainer.querySelector("[autofocus]"); + if ( + !autofocusedElement || + document.activeElement !== autofocusedElement + ) { + innerContainer + .querySelectorAll(focusableElements + ", button:not(.modal-close)")[0] + ?.focus(); } - if (focusTarget) { - afterTransition(() => focusTarget.focus()); + + return; + } + + focusableElements = focusableElements + ", button:enabled"; + const firstFocusableElement = innerContainer.querySelectorAll( + focusableElements + )?.[0]; + const focusableContent = innerContainer.querySelectorAll(focusableElements); + const lastFocusableElement = focusableContent[focusableContent.length - 1]; + + if (event.shiftKey) { + if (document.activeElement === firstFocusableElement) { + lastFocusableElement?.focus(); + event.preventDefault(); + } + } else { + if (document.activeElement === lastFocusableElement) { + ( + innerContainer.querySelector(".modal-close") || firstFocusableElement + )?.focus(); + event.preventDefault(); } } }, diff --git a/app/assets/javascripts/discourse/app/components/future-date-input.js b/app/assets/javascripts/discourse/app/components/future-date-input.js index 75a33161b4f..f673f73ae5c 100644 --- a/app/assets/javascripts/discourse/app/components/future-date-input.js +++ b/app/assets/javascripts/discourse/app/components/future-date-input.js @@ -1,50 +1,34 @@ import { and, empty, equal } from "@ember/object/computed"; -import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import Component from "@ember/component"; import { FORMAT } from "select-kit/components/future-date-input-selector"; import I18n from "I18n"; export default Component.extend({ selection: null, - date: null, - time: null, includeDateTime: true, isCustom: equal("selection", "pick_date_and_time"), displayDateAndTimePicker: and("includeDateTime", "isCustom"), displayLabel: null, labelClasses: null, + timeInputDisabled: empty("_date"), - timeInputDisabled: empty("date"), + _date: null, + _time: null, init() { this._super(...arguments); + if (this.input) { const datetime = moment(this.input); this.setProperties({ selection: "pick_date_and_time", - date: datetime.format("YYYY-MM-DD"), - time: datetime.format("HH:mm"), + _date: datetime.format("YYYY-MM-DD"), + _time: datetime.format("HH:mm"), }); } }, - @observes("date", "time") - _updateInput() { - if (!this.date) { - this.set("time", null); - } - - const time = this.time ? ` ${this.time}` : ""; - const dateTime = moment(`${this.date}${time}`); - - if (dateTime.isValid()) { - this.attrs.onChangeInput && - this.attrs.onChangeInput(dateTime.format(FORMAT)); - } else { - this.attrs.onChangeInput && this.attrs.onChangeInput(null); - } - }, - didReceiveAttrs() { this._super(...arguments); @@ -52,4 +36,32 @@ export default Component.extend({ this.set("displayLabel", I18n.t(this.label)); } }, + + @action + onChangeDate(date) { + if (!date) { + this.set("time", null); + } + + this._dateTimeChanged(date, this.time); + }, + + @action + onChangeTime(time) { + if (this._date) { + this._dateTimeChanged(this._date, time); + } + }, + + _dateTimeChanged(date, time) { + time = time ? ` ${time}` : ""; + const dateTime = moment(`${date}${time}`); + + if (dateTime.isValid()) { + this.attrs.onChangeInput && + this.attrs.onChangeInput(dateTime.format(FORMAT)); + } else { + this.attrs.onChangeInput && this.attrs.onChangeInput(null); + } + }, }); diff --git a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js new file mode 100644 index 00000000000..6fd51382092 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js @@ -0,0 +1,133 @@ +const TIMEFRAME_BASE = { + enabled: () => true, + when: () => null, + icon: "briefcase", + displayWhen: true, +}; + +function buildTimeframe(opts) { + return jQuery.extend({}, TIMEFRAME_BASE, opts); +} + +const TIMEFRAMES = [ + buildTimeframe({ + id: "now", + format: "h:mm a", + enabled: (opts) => opts.canScheduleNow, + when: (time) => time.add(1, "minute"), + icon: "magic", + }), + buildTimeframe({ + id: "later_today", + format: "h a", + enabled: (opts) => opts.canScheduleToday, + when: (time) => time.hour(18).minute(0), + icon: "far-moon", + }), + buildTimeframe({ + id: "tomorrow", + format: "ddd, h a", + when: (time, timeOfDay) => time.add(1, "day").hour(timeOfDay).minute(0), + icon: "far-sun", + }), + buildTimeframe({ + id: "later_this_week", + format: "ddd, h a", + enabled: (opts) => !opts.canScheduleToday && opts.day > 0 && opts.day < 4, + when: (time, timeOfDay) => time.add(2, "day").hour(timeOfDay).minute(0), + }), + buildTimeframe({ + id: "this_weekend", + format: "ddd, h a", + enabled: (opts) => opts.day > 0 && opts.day < 5 && opts.includeWeekend, + when: (time, timeOfDay) => time.day(6).hour(timeOfDay).minute(0), + icon: "bed", + }), + buildTimeframe({ + id: "next_week", + format: "ddd, h a", + enabled: (opts) => opts.day !== 0, + when: (time, timeOfDay) => + time.add(1, "week").day(1).hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "two_weeks", + format: "MMM D", + when: (time, timeOfDay) => time.add(2, "week").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "next_month", + format: "MMM D", + enabled: (opts) => opts.now.date() !== moment().endOf("month").date(), + when: (time, timeOfDay) => + time.add(1, "month").startOf("month").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "two_months", + format: "MMM D", + enabled: (opts) => opts.includeMidFuture, + when: (time, timeOfDay) => + time.add(2, "month").startOf("month").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "three_months", + format: "MMM D", + enabled: (opts) => opts.includeMidFuture, + when: (time, timeOfDay) => + time.add(3, "month").startOf("month").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "four_months", + format: "MMM D", + enabled: (opts) => opts.includeMidFuture, + when: (time, timeOfDay) => + time.add(4, "month").startOf("month").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "six_months", + format: "MMM D", + enabled: (opts) => opts.includeMidFuture, + when: (time, timeOfDay) => + time.add(6, "month").startOf("month").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "one_year", + format: "MMM D", + enabled: (opts) => opts.includeFarFuture, + when: (time, timeOfDay) => + time.add(1, "year").startOf("day").hour(timeOfDay).minute(0), + icon: "briefcase", + }), + buildTimeframe({ + id: "forever", + enabled: (opts) => opts.includeFarFuture, + when: (time, timeOfDay) => time.add(1000, "year").hour(timeOfDay).minute(0), + icon: "gavel", + displayWhen: false, + }), + buildTimeframe({ + id: "pick_date_and_time", + enabled: (opts) => opts.includeDateTime, + icon: "far-calendar-plus", + }), +]; + +let _timeframeById = null; +export function timeframeDetails(id) { + if (!_timeframeById) { + _timeframeById = {}; + TIMEFRAMES.forEach((t) => (_timeframeById[t.id] = t)); + } + return _timeframeById[id]; +} + +export default function buildTimeframes(options = {}) { + return TIMEFRAMES.filter((tf) => tf.enabled(options)); +} diff --git a/app/assets/javascripts/discourse/app/templates/components/bread-crumbs.hbs b/app/assets/javascripts/discourse/app/templates/components/bread-crumbs.hbs index df1de24a368..f59fa8155f2 100644 --- a/app/assets/javascripts/discourse/app/templates/components/bread-crumbs.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/bread-crumbs.hbs @@ -1,37 +1,43 @@ {{#each categoryBreadcrumbs as |breadcrumb|}} {{#if breadcrumb.hasOptions}} - {{category-drop - category=breadcrumb.category - categories=breadcrumb.options - tagId=tag.id - editingCategory=editingCategory - editingCategoryTab=editingCategoryTab - options=(hash - parentCategory=breadcrumb.parentCategory - subCategory=breadcrumb.isSubcategory - noSubcategories=breadcrumb.noSubcategories - autoFilterable=true - ) - }} +
-This is another test This is a great title
http://www.example.com/no-title.html
-This is another test http://www.example.com/no-title.html
-This is another test This is a great title
http://www.example.com/no-title.html
+This is another test http://www.example.com/no-title.html
+This is another test This is a great title
Test www.example.com/page
' + 'Test www.example.com/page
' ); await fillIn(".d-editor-input", `Test www.example.com/page Test`); assert.equal(requestsCount, 1); assert.equal( queryAll(".d-editor-preview").html().trim(), - 'Test www.example.com/page Test
' + 'Test www.example.com/page Test
' ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 190017f2da6..5f612ef6c5d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -750,12 +750,8 @@ acceptance("Composer", function (needs) { await click("button.compose-pm"); await click(".modal .btn-default"); - assert.equal( - queryAll("#private-message-users .selected-name:nth-of-type(1)") - .text() - .trim(), - "codinghorror" - ); + const privateMessageUsers = selectKit("#private-message-users"); + assert.equal(privateMessageUsers.header().value(), "codinghorror"); } finally { toggleCheckDraftPopup(false); } diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js index 55105ba1630..d12ec51292e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js @@ -4,7 +4,6 @@ import { count, exists, fakeTime, - query, queryAll, } from "discourse/tests/helpers/qunit-helpers"; import { test } from "qunit"; @@ -231,14 +230,6 @@ acceptance( await click(".modal-footer .show-advanced"); await click(".future-date-input-selector-header"); - assert.equal( - query(".future-date-input-selector-header").getAttribute( - "aria-expanded" - ), - "true", - "selector is expanded" - ); - const options = Array.from( queryAll(`ul.select-kit-collection li span.name`).map((_, x) => x.innerText.trim() diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js index ef2e6a0d0c0..0637892b675 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js @@ -89,7 +89,7 @@ acceptance("Managing Group Membership", function (needs) { ); await emailDomains.expand(); await emailDomains.fillInFilter("foo.com"); - await emailDomains.keyboard("Enter"); + await emailDomains.selectRowByValue("foo.com"); assert.equal(emailDomains.header().value(), "foo.com"); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-test.js index 96432613ef5..d8197e6688d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-test.js @@ -212,8 +212,9 @@ acceptance("Group - Authenticated", function (needs) { await click(".group-message-button"); assert.equal(count("#reply-control"), 1, "it opens the composer"); + const privateMessageUsers = selectKit("#private-message-users"); assert.equal( - queryAll("#private-message-users .selected-name").text().trim(), + privateMessageUsers.header().value(), "discourse", "it prefills the group name" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js index e1afdc715f3..72ce0b2101f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js @@ -132,10 +132,9 @@ acceptance("Modal Keyboard Events", function (needs) { test("modal-keyboard-events", async function (assert) { await visit("/t/internationalization-localization/280"); - await click(".toggle-admin-menu"); await click(".admin-topic-timer-update button"); - await triggerKeyEvent(".d-modal", "keyup", 13); + await triggerKeyEvent(".d-modal", "keydown", 13); assert.equal( count("#modal-alert:visible"), @@ -148,14 +147,16 @@ acceptance("Modal Keyboard Events", function (needs) { "hitting Enter does not dismiss modal due to alert error" ); - await triggerKeyEvent("#main-outlet", "keyup", 27); + assert.ok(exists(".d-modal:visible"), "modal should be visible"); + + await triggerKeyEvent("#main-outlet", "keydown", 27); + assert.ok(!exists(".d-modal:visible"), "ESC should close the modal"); await click(".topic-body button.reply"); - await click(".d-editor-button-bar .btn.link"); + await triggerKeyEvent(".d-modal", "keydown", 13); - await triggerKeyEvent(".d-modal", "keyup", 13); assert.ok( !exists(".d-modal:visible"), "modal should disappear on hitting Enter" diff --git a/app/assets/javascripts/discourse/tests/acceptance/new-message-test.js b/app/assets/javascripts/discourse/tests/acceptance/new-message-test.js index b5ef162d4fc..96ffed16843 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/new-message-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/new-message-test.js @@ -1,3 +1,4 @@ +import selectKit from "discourse/tests/helpers/select-kit-helper"; import { acceptance, exists, @@ -35,10 +36,10 @@ acceptance("New Message - Authenticated", function (needs) { "message body", "it pre-fills message body" ); + + const privateMessageUsers = selectKit("#private-message-users"); assert.equal( - queryAll("#private-message-users .selected-name:nth-of-type(1)") - .text() - .trim(), + privateMessageUsers.header().value(), "charlie", "it selects correct username" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js index 0d5f64bcda7..d0b2d9029e6 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js @@ -117,7 +117,11 @@ acceptance("User Preferences", function (needs) { await savePreferences(); await click(".preferences-nav .nav-categories a"); - await fillIn(".tracking-controls .category-selector input", "faq"); + const categorySelector = selectKit( + ".tracking-controls .category-selector " + ); + await categorySelector.expand(); + await categorySelector.fillInFilter("faq"); await savePreferences(); assert.ok( @@ -510,8 +514,9 @@ acceptance("Security", function (needs) { "it should display three tokens" ); - await click(".auth-token-dropdown button:nth-of-type(1)"); - await click("li[data-value='notYou']"); + const authTokenDropdown = selectKit(".auth-token-dropdown"); + await authTokenDropdown.expand(); + await authTokenDropdown.selectRowByValue("notYou"); assert.equal(count(".d-modal:visible"), 1, "modal should appear"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/review-test.js b/app/assets/javascripts/discourse/tests/acceptance/review-test.js index 23c7b64c139..b5a04482412 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/review-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/review-test.js @@ -44,11 +44,14 @@ acceptance("Review", function (needs) { }); test("Reject user", async function (assert) { - await visit("/review"); - await click( - `${user} .reviewable-actions button[data-name="Delete User..."]` + let reviewableActionDropdown = selectKit( + `${user} .reviewable-action-dropdown` ); - await click(`${user} li[data-value="reject_user_delete"]`); + + await visit("/review"); + await reviewableActionDropdown.expand(); + await reviewableActionDropdown.selectRowByValue("reject_user_delete"); + assert.ok( queryAll(".reject-reason-reviewable-modal:visible .title") .html() @@ -57,11 +60,9 @@ acceptance("Review", function (needs) { ); await click(".modal-footer button[aria-label='cancel']"); + await reviewableActionDropdown.expand(); + await reviewableActionDropdown.selectRowByValue("reject_user_block"); - await click( - `${user} .reviewable-actions button[data-name="Delete User..."]` - ); - await click(`${user} li[data-value="reject_user_block"]`); assert.ok( queryAll(".reject-reason-reviewable-modal:visible .title") .html() diff --git a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js index 3de7f851df1..79a6f9777a1 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js @@ -50,9 +50,9 @@ acceptance("Tag Groups", function (needs) { await tags.selectRowByValue("monkey"); await click(".tag-group-content .btn.btn-primary"); await click(".tag-groups-sidebar li:first-child a"); - await tags.expand(); - await click(".group-tags-list .tag-chooser .choice:nth-of-type(1)"); + await tags.expand(); + await tags.deselectItemByValue("monkey"); assert.ok(!query(".tag-group-content .btn.btn-danger").disabled); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index 02b253cceb4..adc5277edb8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -1,3 +1,4 @@ +import selectKit from "discourse/tests/helpers/select-kit-helper"; import { acceptance, count, @@ -412,11 +413,14 @@ acceptance("Tag info", function (needs) { assert.ok(exists(".tag-info .tag-name"), "show tag"); await click("#edit-synonyms"); - await click("#add-synonyms .filter-input"); - assert.equal(count(".tag-chooser-row"), 2); + const addSynonymsDropdown = selectKit("#add-synonyms"); + await addSynonymsDropdown.expand(); + assert.deepEqual( - Array.from(find(".tag-chooser-row")).map((x) => x.dataset["value"]), + Array.from(addSynonymsDropdown.rows()).map((r) => { + return r.dataset.value; + }), ["monkey", "not-monkey"] ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js index 51f63142537..f2c445a907f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js @@ -1,7 +1,6 @@ import { acceptance, fakeTime, - query, queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -45,12 +44,6 @@ acceptance("Topic - Set Slow Mode", function (needs) { await click(".future-date-input-selector-header"); - assert.equal( - query(".future-date-input-selector-header").getAttribute("aria-expanded"), - "true", - "selector is expanded" - ); - const options = Array.from( queryAll(`ul.select-kit-collection li span.name`).map((_, x) => x.innerText.trim() diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-slow-mode-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-slow-mode-test.js index b752e38fc4f..8b1449cfbac 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-slow-mode-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-slow-mode-test.js @@ -1,3 +1,4 @@ +import selectKit from "discourse/tests/helpers/select-kit-helper"; import { acceptance, exists, @@ -38,16 +39,9 @@ acceptance("Topic - Slow Mode - enabled", function (needs) { await click(".toggle-admin-menu"); await click(".topic-admin-slow-mode button"); - await click(".future-date-input-selector-header"); - + const slowModeType = selectKit(".slow-mode-type"); assert.equal( - query(".future-date-input-selector-header").getAttribute("aria-expanded"), - "true", - "selector is expanded" - ); - - assert.equal( - query("div.slow-mode-type span.name").innerText, + slowModeType.header().name(), I18n.t("topic.slow_mode_update.durations.10_minutes"), "slow mode interval is rendered" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js index 421c5fdc348..c380d9ba9d9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js @@ -69,27 +69,11 @@ acceptance("Topic", function (needs) { "it fills composer with the ring string" ); - const targets = queryAll( - "#private-message-users .selected-name", - ".composer-fields" - ); - + const privateMessageUsers = selectKit("#private-message-users"); assert.equal( - $(targets[0]).text().trim(), - "someguy", - "it fills up the composer with the right user to start the PM to" - ); - - assert.equal( - $(targets[1]).text().trim(), - "test", - "it fills up the composer with the right user to start the PM to" - ); - - assert.equal( - $(targets[2]).text().trim(), - "Group", - "it fills up the composer with the right group to start the PM to" + privateMessageUsers.header().value(), + "someguy,test,Group", + "it fills up the composer correctly" ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-interface-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-interface-test.js index 8a79e7de8b7..46252a0a9a8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-interface-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-interface-test.js @@ -28,12 +28,13 @@ acceptance("User Preferences - Interface", function (needs) { await visit("/u/eviltrout/preferences/interface"); // Live changes without reload - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("larger"); + const textSize = selectKit(".text-size .combo-box"); + await textSize.expand(); + await textSize.selectRowByValue("larger"); assert.ok(document.documentElement.classList.contains("text-size-larger")); - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("largest"); + await textSize.expand(); + await textSize.selectRowByValue("largest"); assert.ok(document.documentElement.classList.contains("text-size-largest")); assert.equal(cookie("text_size"), null, "cookie is not set"); @@ -43,16 +44,16 @@ acceptance("User Preferences - Interface", function (needs) { assert.equal(cookie("text_size"), null, "cookie is not set"); - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("larger"); + await textSize.expand(); + await textSize.selectRowByValue("larger"); await click(".text-size input[type=checkbox]"); await savePreferences(); assert.equal(cookie("text_size"), "larger|1", "cookie is set"); await click(".text-size input[type=checkbox]"); - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("largest"); + await textSize.expand(); + await textSize.selectRowByValue("largest"); await savePreferences(); assert.equal(cookie("text_size"), null, "cookie is removed"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js index 5173934d2fa..d827790294b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js @@ -3,7 +3,6 @@ import { count, exists, fakeTime, - query, queryAll, } from "discourse/tests/helpers/qunit-helpers"; import { click, visit } from "@ember/test-helpers"; @@ -130,12 +129,6 @@ acceptance("User Notifications - Users - Ignore User", function (needs) { await click("div.user-notifications div div button"); await click(".future-date-input-selector-header"); - assert.equal( - query(".future-date-input-selector-header").getAttribute("aria-expanded"), - "true", - "selector is expanded" - ); - const options = Array.from( queryAll(`ul.select-kit-collection li span.name`).map((_, x) => x.innerText.trim() diff --git a/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js b/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js index 8ac77253c38..6785b6e1c85 100644 --- a/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js +++ b/app/assets/javascripts/discourse/tests/helpers/select-kit-helper.js @@ -290,12 +290,8 @@ export default function selectKit(selector) { ); }, - async deselectItem(value) { - await click( - queryAll(selector) - .find(".select-kit-header") - .find(`[data-value="${value}"]`)[0] - ); + async deselectItemByValue(value) { + await click(`${selector} .selected-content [data-value="${value}"]`); }, exists() { diff --git a/app/assets/javascripts/discourse/tests/integration/components/future-date-input-selector-test.js b/app/assets/javascripts/discourse/tests/integration/components/future-date-input-selector-test.js new file mode 100644 index 00000000000..d3b396b6f9c --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/future-date-input-selector-test.js @@ -0,0 +1,84 @@ +import selectKit from "discourse/tests/helpers/select-kit-helper"; +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + exists, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import I18n from "I18n"; + +discourseModule( + "Unit | Lib | select-kit/future-date-input-selector", + function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); + + hooks.afterEach(function () { + if (this.clock) { + this.clock.restore(); + } + }); + + componentTest("rendering and expanding", { + template: hbs` + {{future-date-input-selector + options=(hash + none="topic.auto_update_input.none" + ) + }} + `, + + async test(assert) { + assert.ok( + exists(".future-date-input-selector"), + "Selector is rendered" + ); + + assert.ok( + this.subject.header().label() === + I18n.t("topic.auto_update_input.none"), + "Default text is rendered" + ); + + await this.subject.expand(); + + assert.ok( + exists(".select-kit-collection"), + "List of options is rendered" + ); + }, + }); + + componentTest("shows 'Custom date and time' if it's enabled", { + template: hbs` + {{future-date-input-selector + includeDateTime=true + }} + `, + + async test(assert) { + await this.subject.expand(); + const options = getOptions(); + const customDateAndTime = I18n.t( + "topic.auto_update_input.pick_date_and_time" + ); + + assert.ok(options.includes(customDateAndTime)); + }, + }); + + function getOptions() { + return Array.from( + queryAll(`.select-kit-collection .select-kit-row`).map( + (_, span) => span.dataset.name + ) + ); + } + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js b/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js index 965b4800236..f35ea125fae 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js @@ -1,5 +1,5 @@ import { set } from "@ember/object"; -import { click, fillIn } from "@ember/test-helpers"; +import { click } from "@ember/test-helpers"; import User from "discourse/models/user"; import componentTest, { setupRenderingTest, @@ -40,7 +40,7 @@ discourseModule("Integration | Component | invite-panel", function (hooks) { async test(assert) { const input = selectKit(".invite-user-input"); await input.expand(); - await fillIn(".invite-user-input .filter-input", "eviltrout@example.com"); + await input.fillInFilter("eviltrout@example.com"); await input.selectRowByValue("eviltrout@example.com"); assert.ok(!exists(".send-invite:disabled")); await click(".generate-invite-link"); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js index e0446a7a156..a49c4d1dae2 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js @@ -63,15 +63,9 @@ discourseModule( async test(assert) { await this.subject.expand(); - assert.equal( - this.subject.rowByIndex(0).title(), - "Discussion about features or potential features of Discourse: how they work, why they work, etc." - ); + assert.equal(this.subject.rowByIndex(0).title(), "feature"); assert.equal(this.subject.rowByIndex(0).value(), 2); - assert.equal( - this.subject.rowByIndex(1).title(), - "My idea here is to have mini specs for features we would like built but have no bandwidth to build" - ); + assert.equal(this.subject.rowByIndex(1).title(), "spec"); assert.equal(this.subject.rowByIndex(1).value(), 26); assert.equal( this.subject.rows().length, @@ -320,7 +314,8 @@ discourseModule( await this.subject.expand(); assert.equal( - this.subject.rowByIndex(0).el()[0].title, + this.subject.rowByIndex(0).el()[0].querySelector(".category-desc") + .innerText, 'baz "bar ‘foo’' ); }, diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/email-group-user-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/email-group-user-chooser-test.js deleted file mode 100644 index 78b122d60dd..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/email-group-user-chooser-test.js +++ /dev/null @@ -1,64 +0,0 @@ -import componentTest, { - setupRenderingTest, -} from "discourse/tests/helpers/component-test"; -import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; -import hbs from "htmlbars-inline-precompile"; -import selectKit from "discourse/tests/helpers/select-kit-helper"; - -discourseModule( - "Integration | Component | select-kit/email-group-user-chooser", - function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("subject", selectKit()); - this.setProperties({ - value: [], - onChange() {}, - }); - }); - - componentTest("autofocus option set to true", { - template: hbs`{{email-group-user-chooser - value=value - onChange=onChange - options=(hash - autofocus=true - ) - }}`, - - async test(assert) { - this.subject; - assert.ok( - this.subject.header().el()[0].classList.contains("is-focused"), - "select-kit header has is-focused class" - ); - assert.ok( - this.subject.filter().el()[0].querySelector(".filter-input") - .autofocus, - "filter input has autofocus attribute" - ); - }, - }); - - componentTest("without autofocus", { - template: hbs`{{email-group-user-chooser - value=value - onChange=onChange - }}`, - - async test(assert) { - this.subject; - assert.ok( - !this.subject.header().el()[0].classList.contains("is-focused"), - "select-kit header doesn't have is-focused class" - ); - assert.ok( - !this.subject.filter().el()[0].querySelector(".filter-input") - .autofocus, - "filter input doesn't have autofocus attribute" - ); - }, - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-selector-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-selector-test.js deleted file mode 100644 index ad368c5ef5c..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-selector-test.js +++ /dev/null @@ -1,300 +0,0 @@ -import componentTest, { - setupRenderingTest, -} from "discourse/tests/helpers/component-test"; -import { - discourseModule, - exists, - fakeTime, - query, - queryAll, -} from "discourse/tests/helpers/qunit-helpers"; -import hbs from "htmlbars-inline-precompile"; -import selectKit from "discourse/tests/helpers/select-kit-helper"; -import I18n from "I18n"; - -discourseModule( - "Integration | Component | select-kit/future-date-input-selector", - function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("subject", selectKit()); - }); - - hooks.afterEach(function () { - if (this.clock) { - this.clock.restore(); - } - }); - - componentTest("rendering and expanding", { - template: hbs` - {{future-date-input-selector - options=(hash - none="topic.auto_update_input.none" - ) - }} - `, - - async test(assert) { - assert.ok( - exists("div.future-date-input-selector"), - "Selector is rendered" - ); - - assert.ok( - query("span").innerText === I18n.t("topic.auto_update_input.none"), - "Default text is rendered" - ); - - await this.subject.expand(); - - assert.equal( - query(".future-date-input-selector-header").getAttribute( - "aria-expanded" - ), - "true", - "selector is expanded" - ); - - assert.ok( - exists("ul.select-kit-collection"), - "List of options is rendered" - ); - }, - }); - - componentTest("shows default options", { - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-06-07T08:00:00", timezone, true); // Monday - }, - - async test(assert) { - await this.subject.expand(); - - const options = getOptions(); - const expected = [ - I18n.t("topic.auto_update_input.later_today"), - I18n.t("topic.auto_update_input.tomorrow"), - I18n.t("topic.auto_update_input.next_week"), - I18n.t("topic.auto_update_input.two_weeks"), - I18n.t("topic.auto_update_input.next_month"), - I18n.t("topic.auto_update_input.two_months"), - I18n.t("topic.auto_update_input.three_months"), - I18n.t("topic.auto_update_input.four_months"), - I18n.t("topic.auto_update_input.six_months"), - ]; - assert.deepEqual(options, expected); - }, - }); - - componentTest("doesn't show 'Next Week' on Sundays", { - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-06-13T08:00:00", timezone, true); // Sunday - }, - - async test(assert) { - await this.subject.expand(); - - const options = getOptions(); - const nextWeek = I18n.t("topic.auto_update_input.next_week"); - assert.not(options.includes(nextWeek)); - }, - }); - - componentTest("shows 'Custom date and time' if it's enabled", { - template: hbs` - {{future-date-input-selector - includeDateTime=true - }} - `, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const customDateAndTime = I18n.t( - "topic.auto_update_input.pick_date_and_time" - ); - - assert.ok(options.includes(customDateAndTime)); - }, - }); - - componentTest("shows 'This Weekend' if it's enabled", { - template: hbs` - {{future-date-input-selector - includeWeekend=true - }} - `, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-06-07T08:00:00", timezone, true); // Monday - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const thisWeekend = I18n.t("topic.auto_update_input.this_weekend"); - - assert.ok(options.includes(thisWeekend)); - }, - }); - - componentTest("doesn't show 'This Weekend' on Fridays", { - template: hbs` - {{future-date-input-selector - includeWeekend=true - }} - `, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-23 18:00:00", timezone, true); // Friday - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const thisWeekend = I18n.t("topic.auto_update_input.this_weekend"); - - assert.not(options.includes(thisWeekend)); - }, - }); - - componentTest("doesn't show 'This Weekend' on Sundays", { - /* - We need this test to avoid regressions. - We tend to write such conditions and think that - they mean the beginning of work week - (Monday, Tuesday and Wednesday in this specific case): - - if (date.day <= 3) { - ... - } - - In fact, Sunday will pass this check too, because - in moment.js 0 stands for Sunday. - */ - - template: hbs` - {{future-date-input-selector - includeWeekend=true - }} - `, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-25 18:00:00", timezone, true); // Sunday - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const thisWeekend = I18n.t("topic.auto_update_input.this_weekend"); - - assert.not(options.includes(thisWeekend)); - }, - }); - - componentTest( - "shows 'Later This Week' instead of 'Later Today' at the end of the day", - { - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-19 18:00:00", timezone, true); // Monday evening - }, - - async test(assert) { - await this.subject.expand(); - - const options = getOptions(); - const laterToday = I18n.t("topic.auto_update_input.later_today"); - const laterThisWeek = I18n.t( - "topic.auto_update_input.later_this_week" - ); - - assert.not(options.includes(laterToday)); - assert.ok(options.includes(laterThisWeek)); - }, - } - ); - - componentTest("doesn't show 'Later This Week' on Tuesdays", { - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-22 18:00:00", timezone, true); // Tuesday evening - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const laterThisWeek = I18n.t("topic.auto_update_input.later_this_week"); - assert.not(options.includes(laterThisWeek)); - }, - }); - - componentTest("doesn't show 'Later This Week' on Sundays", { - /* We need this test to avoid regressions. - We tend to write such conditions and think that - they mean the beginning of business week - (Monday, Tuesday and Wednesday in this specific case): - - if (date.day < 3) { - ... - } - - In fact, Sunday will pass this check too, because - in moment.js 0 stands for Sunday. */ - - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-25 18:00:00", timezone, true); // Sunday evening - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const laterThisWeek = I18n.t("topic.auto_update_input.later_this_week"); - assert.not(options.includes(laterThisWeek)); - }, - }); - - componentTest("doesn't show 'Next Month' on the last day of the month", { - template: hbs`{{future-date-input-selector}}`, - - beforeEach() { - const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-30 18:00:00", timezone, true); // The last day of April - }, - - async test(assert) { - await this.subject.expand(); - const options = getOptions(); - const nextMonth = I18n.t("topic.auto_update_input.next_month"); - - assert.not(options.includes(nextMonth)); - }, - }); - - function getOptions() { - return Array.from( - queryAll(`ul.select-kit-collection li span.name`).map((_, span) => - span.innerText.trim() - ) - ); - } - } -); 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 c9a5bbcd71e..f6515e86149 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 @@ -45,7 +45,7 @@ discourseModule( assert.equal(queryAll(".select-kit-row").text().trim(), "monkey x1"); await this.subject.fillInFilter("key"); assert.equal(queryAll(".select-kit-row").text().trim(), "monkey x1"); - await this.subject.keyboard("Enter"); + await this.subject.selectRowByValue("monkey"); assert.equal(this.subject.header().value(), "foo,bar,monkey"); }, @@ -64,7 +64,7 @@ discourseModule( await this.subject.expand(); await this.subject.fillInFilter("baz"); - await this.subject.keyboard("Enter"); + await this.subject.selectRowByValue("monkey"); const error = queryAll(".select-kit-error").text(); assert.equal( diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js index 54dee262543..22cd0c624b7 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/user-chooser-test.js @@ -34,7 +34,8 @@ discourseModule( }, async test(assert) { - await this.subject.deselectItem("bob"); + await this.subject.expand(); + await this.subject.deselectItemByValue("bob"); assert.equal(this.subject.header().name(), "martin"); }, }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js index 45945469839..3e86f3294ad 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js @@ -109,7 +109,7 @@ discourseModule("Integration | Component | value-list", function (hooks) { await selectKit().expand(); await selectKit().fillInFilter("eviltrout"); - await selectKit().keyboard("Enter"); + await selectKit().selectRowByValue("eviltrout"); assert.equal( count(".values .value"), diff --git a/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js new file mode 100644 index 00000000000..cb72b3a3090 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js @@ -0,0 +1,164 @@ +import { module, test } from "qunit"; +import { fakeTime } from "discourse/tests/helpers/qunit-helpers"; +import buildTimeframes from "discourse/lib/timeframes-builder"; + +const DEFAULT_OPTIONS = { + includeWeekend: null, + includeMidFuture: true, + includeFarFuture: null, + includeDateTime: null, + canScheduleNow: false, +}; + +function buildOptions(now, opts) { + return Object.assign( + {}, + DEFAULT_OPTIONS, + { now, day: now.day(), canScheduleToday: 24 - now.hour() > 6 }, + opts + ); +} + +module("Unit | Lib | timeframes-builder", function () { + test("default options", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-06-07T08:00:00", timezone, true); // Monday + + const expected = [ + "later_today", + "tomorrow", + "next_week", + "two_weeks", + "next_month", + "two_months", + "three_months", + "four_months", + "six_months", + ]; + + assert.deepEqual( + buildTimeframes(buildOptions(moment())).mapBy("id"), + expected + ); + + clock.restore(); + }); + + test("doesn't output 'Next Week' on Sundays", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-06-13T08:00:00", timezone, true); // Sunday + + assert.ok( + !buildTimeframes(buildOptions(moment())).mapBy("id").includes("next_week") + ); + + clock.restore(); + }); + + test("outputs 'This Weekend' if it's enabled", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-06-07T08:00:00", timezone, true); // Monday + + assert.ok( + buildTimeframes(buildOptions(moment(), { includeWeekend: true })) + .mapBy("id") + .includes("this_weekend") + ); + + clock.restore(); + }); + + test("doesn't output 'This Weekend' on Fridays", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-23 18:00:00", timezone, true); // Friday + + assert.ok( + !buildTimeframes(buildOptions(moment(), { includeWeekend: true })) + .mapBy("id") + .includes("this_weekend") + ); + + clock.restore(); + }); + + test("doesn't show 'This Weekend' on Sundays", function (assert) { + /* + We need this test to avoid regressions. + We tend to write such conditions and think that + they mean the beginning of work week + (Monday, Tuesday and Wednesday in this specific case): + + if (date.day <= 3) { + ... + } + + In fact, Sunday will pass this check too, because + in moment.js 0 stands for Sunday. + */ + + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-25 18:00:00", timezone, true); // Sunday + + assert.ok( + !buildTimeframes(buildOptions(moment(), { includeWeekend: true })) + .mapBy("id") + .includes("this_weekend") + ); + + clock.restore(); + }); + + test("outputs 'Later This Week' instead of 'Later Today' at the end of the day", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-19 18:00:00", timezone, true); // Monday evening + const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); + + assert.not(timeframes.includes("later_today")); + assert.ok(timeframes.includes("later_this_week")); + + clock.restore(); + }); + + test("doesn't output 'Later This Week' on Tuesdays", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-22 18:00:00", timezone, true); // Tuesday evening + const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); + + assert.not(timeframes.includes("later_this_week")); + + clock.restore(); + }); + + test("doesn't output 'Later This Week' on Sundays", function (assert) { + /* + We need this test to avoid regressions. + We tend to write such conditions and think that + they mean the beginning of business week + (Monday, Tuesday and Wednesday in this specific case): + + if (date.day < 3) { + ... + } + + In fact, Sunday will pass this check too, because + in moment.js 0 stands for Sunday. + */ + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-25 18:00:00", timezone, true); // Sunday evening + const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); + + assert.not(timeframes.includes("later_this_week")); + + clock.restore(); + }); + + test("doesn't output 'Next Month' on the last day of the month", function (assert) { + const timezone = moment.tz.guess(); + const clock = fakeTime("2100-04-30 18:00:00", timezone, true); // The last day of April + const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); + + assert.not(timeframes.includes("next_month")); + + clock.restore(); + }); +}); diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop.js b/app/assets/javascripts/select-kit/addon/components/category-drop.js index 93083db0b37..7723bdc2c17 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-drop.js +++ b/app/assets/javascripts/select-kit/addon/components/category-drop.js @@ -18,7 +18,6 @@ export default ComboBoxComponent.extend({ classNames: ["category-drop"], value: readOnly("category.id"), content: readOnly("categoriesWithShortcuts.[]"), - tagName: "li", categoryStyle: readOnly("siteSettings.category_style"), noCategoriesLabel: I18n.t("categories.no_subcategory"), navigateToEdit: false, diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.js b/app/assets/javascripts/select-kit/addon/components/category-row.js index dedd8a5c48d..7794a926cc7 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-row.js +++ b/app/assets/javascripts/select-kit/addon/components/category-row.js @@ -7,12 +7,6 @@ import { computed } from "@ember/object"; import layout from "select-kit/templates/components/category-row"; import { setting } from "discourse/lib/computed"; -function htmlToText(encodedString) { - const elem = document.createElement("textarea"); - elem.innerHTML = encodedString; - return elem.value; -} - export default SelectKitRowComponent.extend({ layout, classNames: ["category-row"], @@ -34,19 +28,11 @@ export default SelectKitRowComponent.extend({ } ), - title: computed( - "descriptionText", - "description", - "categoryName", - function () { - if (this.category) { - return htmlToText( - this.descriptionText || this.description || this.categoryName - ); - } + title: computed("categoryName", function () { + if (this.category) { + return this.categoryName; } - ), - + }), categoryName: reads("category.name"), categoryDescription: reads("category.description"), diff --git a/app/assets/javascripts/select-kit/addon/components/category-selector.js b/app/assets/javascripts/select-kit/addon/components/category-selector.js index 1a081f9e9c2..785139da23c 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/category-selector.js @@ -1,4 +1,4 @@ -import EmberObject, { computed, get } from "@ember/object"; +import EmberObject, { computed } from "@ember/object"; import Category from "discourse/models/category"; import I18n from "I18n"; import MultiSelectComponent from "select-kit/components/multi-select"; @@ -17,7 +17,7 @@ export default MultiSelectComponent.extend({ allowAny: false, allowUncategorized: "allowUncategorized", displayCategoryDescription: false, - selectedNameComponent: "multi-select/selected-category", + selectedChoiceComponent: "selected-choice-category", }, init() { @@ -43,13 +43,6 @@ export default MultiSelectComponent.extend({ value: mapBy("categories", "id"), - filterComputedContent(computedContent, filter) { - const regex = new RegExp(filter, "i"); - return computedContent.filter((category) => - this._normalize(get(category, "name")).match(regex) - ); - }, - modifyComponentForRow() { return "category-row"; }, diff --git a/app/assets/javascripts/select-kit/addon/components/create-color-row.js b/app/assets/javascripts/select-kit/addon/components/create-color-row.js index e19630287f0..4f978fe0cb1 100644 --- a/app/assets/javascripts/select-kit/addon/components/create-color-row.js +++ b/app/assets/javascripts/select-kit/addon/components/create-color-row.js @@ -12,7 +12,9 @@ export default SelectKitRowComponent.extend({ schedule("afterRender", () => { const color = escapeExpression(this.rowValue); - this.element.style.borderLeftColor = `#${color}`; + this.element.style.borderLeftColor = color.startsWith("#") + ? color + : `#${color}`; }); }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/dropdown-select-box/dropdown-select-box-header.js b/app/assets/javascripts/select-kit/addon/components/dropdown-select-box/dropdown-select-box-header.js index 1cb0ca1bace..76f6d03d4b2 100644 --- a/app/assets/javascripts/select-kit/addon/components/dropdown-select-box/dropdown-select-box-header.js +++ b/app/assets/javascripts/select-kit/addon/components/dropdown-select-box/dropdown-select-box-header.js @@ -6,11 +6,8 @@ import { readOnly } from "@ember/object/computed"; export default SingleSelectHeaderComponent.extend({ layout, classNames: ["dropdown-select-box-header"], - tagName: "button", classNameBindings: ["btnClassName", "btnStyleClass"], showFullTitle: readOnly("selectKit.options.showFullTitle"), - attributeBindings: ["buttonType:type"], - buttonType: "button", customStyle: readOnly("selectKit.options.customStyle"), btnClassName: computed("showFullTitle", function () { diff --git a/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser-header.js b/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser-header.js deleted file mode 100644 index b25bb6edef6..00000000000 --- a/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser-header.js +++ /dev/null @@ -1,76 +0,0 @@ -import MultiSelectHeaderComponent from "select-kit/components/multi-select/multi-select-header"; -import { computed } from "@ember/object"; -import { gt } from "@ember/object/computed"; -import { isTesting } from "discourse-common/config/environment"; -import layout from "select-kit/templates/components/email-group-user-chooser-header"; - -export default MultiSelectHeaderComponent.extend({ - layout, - classNames: ["email-group-user-chooser-header"], - hasHiddenItems: gt("hiddenItemsCount", 0), - - shownItems: computed("hiddenItemsCount", function () { - if ( - this.selectKit.noneItem === this.selectedContent || - this.hiddenItemsCount === 0 - ) { - return this.selectedContent; - } - return this.selectedContent.slice( - 0, - this.selectedContent.length - this.hiddenItemsCount - ); - }), - - hiddenItemsCount: computed( - "selectedContent.[]", - "selectKit.options.autoWrap", - "selectKit.isExpanded", - function () { - if ( - !this.selectKit.options.autoWrap || - this.selectKit.isExpanded || - this.selectedContent === this.selectKit.noneItem || - this.selectedContent.length <= 1 || - isTesting() - ) { - return 0; - } else { - const selectKitHeaderWidth = this.element.offsetWidth; - const choices = this.element.querySelectorAll(".selected-name.choice"); - const input = this.element.querySelector(".filter-input"); - const alreadyHidden = this.element.querySelector(".x-more-item"); - if (alreadyHidden) { - const hiddenCount = parseInt( - alreadyHidden.getAttribute("data-hidden-count"), - 10 - ); - return ( - hiddenCount + - (this.selectedContent.length - (choices.length + hiddenCount)) - ); - } - if (choices.length === 0 && this.selectedContent.length > 0) { - return 0; - } - let total = choices[0].offsetWidth + input.offsetWidth; - let shownItemsCount = 1; - let shouldHide = false; - for (let i = 1; i < choices.length - 1; i++) { - const currentWidth = choices[i].offsetWidth; - const nextWidth = choices[i + 1].offsetWidth; - const ratio = - (total + currentWidth + nextWidth) / selectKitHeaderWidth; - if (ratio >= 0.95) { - shouldHide = true; - break; - } else { - shownItemsCount++; - total += currentWidth; - } - } - return shouldHide ? choices.length - shownItemsCount : 0; - } - } - ), -}); diff --git a/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser.js b/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser.js index 7923147906e..6a5463ab3b2 100644 --- a/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/email-group-user-chooser.js @@ -12,7 +12,6 @@ export default UserChooserComponent.extend({ }, selectKitOptions: { - headerComponent: "email-group-user-chooser-header", filterComponent: "email-group-user-chooser-filter", fullWidthWrap: false, autoWrap: false, diff --git a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js index 495d87c7dd5..7263a27ca0b 100644 --- a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js @@ -1,139 +1,10 @@ import ComboBoxComponent from "select-kit/components/combo-box"; import DatetimeMixin from "select-kit/components/future-date-input-selector/mixin"; -import I18n from "I18n"; import { computed } from "@ember/object"; import { equal } from "@ember/object/computed"; import { isEmpty } from "@ember/utils"; - -const TIMEFRAME_BASE = { - enabled: () => true, - when: () => null, - icon: "briefcase", - displayWhen: true, -}; - -function buildTimeframe(opts) { - return jQuery.extend({}, TIMEFRAME_BASE, opts); -} - -export const TIMEFRAMES = [ - buildTimeframe({ - id: "now", - format: "h:mm a", - enabled: (opts) => opts.canScheduleNow, - when: (time) => time.add(1, "minute"), - icon: "magic", - }), - buildTimeframe({ - id: "later_today", - format: "h a", - enabled: (opts) => opts.canScheduleToday, - when: (time) => time.hour(18).minute(0), - icon: "far-moon", - }), - buildTimeframe({ - id: "tomorrow", - format: "ddd, h a", - when: (time, timeOfDay) => time.add(1, "day").hour(timeOfDay).minute(0), - icon: "far-sun", - }), - buildTimeframe({ - id: "later_this_week", - format: "ddd, h a", - enabled: (opts) => !opts.canScheduleToday && opts.day > 0 && opts.day < 4, - when: (time, timeOfDay) => time.add(2, "day").hour(timeOfDay).minute(0), - }), - buildTimeframe({ - id: "this_weekend", - format: "ddd, h a", - enabled: (opts) => opts.day > 0 && opts.day < 5 && opts.includeWeekend, - when: (time, timeOfDay) => time.day(6).hour(timeOfDay).minute(0), - icon: "bed", - }), - buildTimeframe({ - id: "next_week", - format: "ddd, h a", - enabled: (opts) => opts.day !== 0, - when: (time, timeOfDay) => - time.add(1, "week").day(1).hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "two_weeks", - format: "MMM D", - when: (time, timeOfDay) => time.add(2, "week").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "next_month", - format: "MMM D", - enabled: (opts) => opts.now.date() !== moment().endOf("month").date(), - when: (time, timeOfDay) => - time.add(1, "month").startOf("month").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "two_months", - format: "MMM D", - enabled: (opts) => opts.includeMidFuture, - when: (time, timeOfDay) => - time.add(2, "month").startOf("month").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "three_months", - format: "MMM D", - enabled: (opts) => opts.includeMidFuture, - when: (time, timeOfDay) => - time.add(3, "month").startOf("month").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "four_months", - format: "MMM D", - enabled: (opts) => opts.includeMidFuture, - when: (time, timeOfDay) => - time.add(4, "month").startOf("month").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "six_months", - format: "MMM D", - enabled: (opts) => opts.includeMidFuture, - when: (time, timeOfDay) => - time.add(6, "month").startOf("month").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "one_year", - format: "MMM D", - enabled: (opts) => opts.includeFarFuture, - when: (time, timeOfDay) => - time.add(1, "year").startOf("day").hour(timeOfDay).minute(0), - icon: "briefcase", - }), - buildTimeframe({ - id: "forever", - enabled: (opts) => opts.includeFarFuture, - when: (time, timeOfDay) => time.add(1000, "year").hour(timeOfDay).minute(0), - icon: "gavel", - displayWhen: false, - }), - buildTimeframe({ - id: "pick_date_and_time", - enabled: (opts) => opts.includeDateTime, - icon: "far-calendar-plus", - }), -]; - -let _timeframeById = null; -export function timeframeDetails(id) { - if (!_timeframeById) { - _timeframeById = {}; - TIMEFRAMES.forEach((t) => (_timeframeById[t.id] = t)); - } - return _timeframeById[id]; -} +import buildTimeframes from "discourse/lib/timeframes-builder"; +import I18n from "I18n"; export const FORMAT = "YYYY-MM-DD HH:mmZ"; @@ -165,7 +36,7 @@ export default ComboBoxComponent.extend(DatetimeMixin, { canScheduleToday: 24 - now.hour() > 6, }; - return TIMEFRAMES.filter((tf) => tf.enabled(opts)).map((tf) => { + return buildTimeframes(opts).map((tf) => { return { id: tf.id, name: I18n.t(`topic.auto_update_input.${tf.id}`), diff --git a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector/mixin.js b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector/mixin.js index c9b64fa5318..83b9d8b41da 100644 --- a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector/mixin.js +++ b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector/mixin.js @@ -1,7 +1,7 @@ import { CLOSE_STATUS_TYPE } from "discourse/controllers/edit-topic-timer"; import Mixin from "@ember/object/mixin"; import { isNone } from "@ember/utils"; -import { timeframeDetails } from "select-kit/components/future-date-input-selector"; +import { timeframeDetails } from "discourse/lib/timeframes-builder"; export default Mixin.create({ _computeIconsForValue(value) { diff --git a/app/assets/javascripts/select-kit/addon/components/group-dropdown.js b/app/assets/javascripts/select-kit/addon/components/group-dropdown.js index a0a12c1a70a..c27dda050ee 100644 --- a/app/assets/javascripts/select-kit/addon/components/group-dropdown.js +++ b/app/assets/javascripts/select-kit/addon/components/group-dropdown.js @@ -9,7 +9,6 @@ export default ComboBoxComponent.extend({ pluginApiIdentifiers: ["group-dropdown"], classNames: ["group-dropdown"], content: reads("groupsWithShortcut"), - tagName: "li", valueProperty: null, nameProperty: null, hasManyGroups: gte("content.length", 10), diff --git a/app/assets/javascripts/select-kit/addon/components/list-setting.js b/app/assets/javascripts/select-kit/addon/components/list-setting.js index da9d9f7d4bd..bd76a84b30c 100644 --- a/app/assets/javascripts/select-kit/addon/components/list-setting.js +++ b/app/assets/javascripts/select-kit/addon/components/list-setting.js @@ -14,7 +14,7 @@ export default MultiSelectComponent.extend({ selectKitOptions: { filterable: true, - selectedNameComponent: "selectedNameComponent", + selectedChoiceComponent: "selectedChoiceComponent", }, modifyComponentForRow(collection) { @@ -27,11 +27,11 @@ export default MultiSelectComponent.extend({ } }, - selectedNameComponent: computed("settingName", function () { + selectedChoiceComponent: computed("settingName", function () { if (this.settingName && this.settingName.indexOf("color") > -1) { - return "selected-color"; + return "selected-choice-color"; } else { - return "selected-name"; + return "selected-choice"; } }), 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 d1c4cfce065..59bc2a959bf 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 @@ -1,15 +1,12 @@ import { empty, or } from "@ember/object/computed"; -import ComboBox from "select-kit/components/combo-box"; -import { ERRORS_COLLECTION } from "select-kit/components/select-kit"; +import MultiSelectComponent from "select-kit/components/multi-select"; import I18n from "I18n"; import TagsMixin from "select-kit/mixins/tags"; import { computed } from "@ember/object"; import { makeArray } from "discourse-common/lib/helpers"; - -const SELECTED_TAGS_COLLECTION = "MINI_TAG_CHOOSER_SELECTED_TAGS"; import { setting } from "discourse/lib/computed"; -export default ComboBox.extend(TagsMixin, { +export default MultiSelectComponent.extend(TagsMixin, { pluginApiIdentifiers: ["mini-tag-chooser"], attributeBindings: ["selectKit.options.categoryId:category-id"], classNames: ["mini-tag-chooser"], @@ -17,20 +14,8 @@ export default ComboBox.extend(TagsMixin, { noTags: empty("value"), maxTagSearchResults: setting("max_tag_search_results"), maxTagsPerTopic: setting("max_tags_per_topic"), - highlightedTag: null, - singleSelect: false, - - collections: computed( - "mainCollection.[]", - "errorsCollection.[]", - "highlightedTag", - function () { - return this._super(...arguments); - } - ), selectKitOptions: { - headerComponent: "mini-tag-chooser/mini-tag-chooser-header", fullWidthOnMobile: true, filterable: true, caretDownIcon: "caretIcon", @@ -41,6 +26,7 @@ export default ComboBox.extend(TagsMixin, { none: "tagging.choose_for_topic", closeOnChange: false, maximum: "maximumSelectedTags", + minimum: "minimumSelectedTags", autoInsertNoneItem: false, }, @@ -52,21 +38,6 @@ export default ComboBox.extend(TagsMixin, { return "tag-row"; }, - modifyComponentForCollection(collection) { - if (collection === SELECTED_TAGS_COLLECTION) { - return "mini-tag-chooser/selected-collection"; - } - }, - - modifyContentForCollection(collection) { - if (collection === SELECTED_TAGS_COLLECTION) { - return { - selectedTags: this.value, - highlightedTag: this.highlightedTag, - }; - } - }, - allowAnyTag: or("allowCreate", "site.can_create_tag"), maximumSelectedTags: computed(function () { @@ -78,7 +49,7 @@ export default ComboBox.extend(TagsMixin, { ); }), - modifyNoSelection() { + minimumSelectedTags: computed(function () { if ( this.selectKit.options.minimum || this.selectKit.options.requiredTagGroups @@ -91,42 +62,18 @@ export default ComboBox.extend(TagsMixin, { ); } } + }), - return this._super(...arguments); - }, - - init() { - this._super(...arguments); - - this.insertAfterCollection(ERRORS_COLLECTION, SELECTED_TAGS_COLLECTION); - }, - - caretIcon: computed("value.[]", function () { + caretIcon: computed("value.[]", "content.[]", function () { const maximum = this.selectKit.options.maximum; return maximum && makeArray(this.value).length >= parseInt(maximum, 10) ? null : "plus"; }), - modifySelection(content) { - const minimum = this.selectKit.options.minimum; - if (minimum && makeArray(this.value).length < parseInt(minimum, 10)) { - const key = - this.selectKit.options.minimumLabel || - "select_kit.min_content_not_reached"; - const label = I18n.t(key, { count: this.selectKit.options.minimum }); - content.title = content.name = content.label = label; - } else { - content.name = content.value = makeArray(this.value).join(","); - content.title = content.label = makeArray(this.value).join(", "); - - if (content.label.length > 32) { - content.label = `${content.label.slice(0, 32)}...`; - } - } - - return content; - }, + content: computed("value.[]", function () { + return makeArray(this.value).map((x) => this.defaultItem(x, x)); + }), search(filter) { const data = { @@ -147,6 +94,10 @@ export default ComboBox.extend(TagsMixin, { }, _transformJson(context, json) { + if (context.isDestroyed || context.isDestroying) { + return []; + } + let results = json.results; context.setProperties({ @@ -158,79 +109,10 @@ export default ComboBox.extend(TagsMixin, { results = results.sort((a, b) => a.text.localeCompare(b.text)); } - results = results + return results .filter((r) => !makeArray(context.tags).includes(r.id)) .map((result) => { return { id: result.text, name: result.text, count: result.count }; }); - - return results; - }, - - select(value) { - this._reset(); - - if (!this.validateSelect(value)) { - return; - } - - const tags = [...new Set(makeArray(this.value).concat(value))]; - this.selectKit.change(tags, tags); - }, - - deselect(value) { - this._reset(); - - const tags = [...new Set(makeArray(this.value).removeObject(value))]; - this.selectKit.change(tags, tags); - }, - - _reset() { - this.clearErrors(); - this.set("highlightedTag", null); - }, - - _onKeydown(event) { - const value = makeArray(this.value); - - if (event.key === "Backspace") { - if (!this.selectKit.filter) { - this._onBackspace(this.value, this.highlightedTag); - } - } else if (event.key === "ArrowLeft") { - if (this.highlightedTag) { - const index = value.indexOf(this.highlightedTag); - const highlightedTag = value[index - 1] - ? value[index - 1] - : value.lastObject; - this.set("highlightedTag", highlightedTag); - } else { - this.set("highlightedTag", value.lastObject); - } - } else if (event.key === "ArrowRight") { - if (this.highlightedTag) { - const index = value.indexOf(this.highlightedTag); - const highlightedTag = value[index + 1] - ? value[index + 1] - : value.firstObject; - this.set("highlightedTag", highlightedTag); - } else { - this.set("highlightedTag", value.firstObject); - } - } else { - this.set("highlightedTag", null); - } - - return true; - }, - - _onBackspace(value, highlightedTag) { - if (value && value.length) { - if (!highlightedTag) { - this.set("highlightedTag", value.lastObject); - } else { - this.deselect(highlightedTag); - } - } }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser/selected-collection.js b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser/selected-collection.js index e8187c687a6..9ccbe516280 100644 --- a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser/selected-collection.js +++ b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser/selected-collection.js @@ -1,54 +1,32 @@ -import { empty, reads } from "@ember/object/computed"; +import { reads } from "@ember/object/computed"; import Component from "@ember/component"; import { computed } from "@ember/object"; import layout from "select-kit/templates/components/mini-tag-chooser/selected-collection"; export default Component.extend({ + tagName: "", + layout, - classNames: [ - "mini-tag-chooser-selected-collection", - "selected-tags", - "shouldHide:hidden", - ], - shouldHide: empty("selectedTags.[]"), + selectedTags: reads("collection.content.selectedTags.[]"), - highlightedTag: reads("collection.content.highlightedTag"), - tags: computed( - "selectedTags.[]", - "highlightedTag", - "selectKit.filter", - function () { - if (!this.selectedTags) { - return []; - } - - let tags = this.selectedTags; - if (tags.length >= 20 && this.selectKit.filter) { - tags = tags.filter((t) => t.indexOf(this.selectKit.filter) >= 0); - } else if (tags.length >= 20) { - tags = tags.slice(0, 20); - } - - tags = tags.map((selectedTag) => { - const classNames = ["selected-tag"]; - if (selectedTag === this.highlightedTag) { - classNames.push("is-highlighted"); - } - - return { - value: selectedTag, - classNames: classNames.join(" "), - }; - }); - - return tags; + tags: computed("selectedTags.[]", "selectKit.filter", function () { + if (!this.selectedTags) { + return []; } - ), - actions: { - deselectTag(tag) { - return this.selectKit.deselect(tag); - }, - }, + let tags = this.selectedTags; + if (tags.length >= 20 && this.selectKit.filter) { + tags = tags.filter((t) => t.indexOf(this.selectKit.filter) >= 0); + } else if (tags.length >= 20) { + tags = tags.slice(0, 20); + } + + return tags.map((selectedTag) => { + return { + value: selectedTag, + classNames: "selected-tag", + }; + }); + }), }); 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 534f6994dd2..ec0e6986cd6 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select.js +++ b/app/assets/javascripts/select-kit/addon/components/multi-select.js @@ -1,7 +1,7 @@ import SelectKitComponent from "select-kit/components/select-kit"; import { computed } from "@ember/object"; -import deprecated from "discourse-common/lib/deprecated"; import { isPresent } from "@ember/utils"; +import { next } from "@ember/runloop"; import layout from "select-kit/templates/components/multi-select"; import { makeArray } from "discourse-common/lib/helpers"; @@ -16,13 +16,21 @@ export default SelectKitComponent.extend({ clearable: true, filterable: true, filterIcon: null, - clearOnClick: true, closeOnChange: false, autoInsertNoneItem: false, headerComponent: "multi-select/multi-select-header", - filterComponent: "multi-select/multi-select-filter", + autoFilterable: true, + caretDownIcon: "caretIcon", + caretUpIcon: "caretIcon", }, + caretIcon: computed("value.[]", function () { + const maximum = this.selectKit.options.maximum; + return maximum && makeArray(this.value).length >= parseInt(maximum, 10) + ? null + : "plus"; + }), + search(filter) { return this._super(filter).filter( (content) => !makeArray(this.selectedContent).includes(content) @@ -68,6 +76,16 @@ export default SelectKitComponent.extend({ }, select(value, item) { + if (this.selectKit.hasSelection && this.selectKit.options.maximum === 1) { + this.selectKit.deselectByValue( + this.getValue(this.selectedContent.firstObject) + ); + next(() => { + this.selectKit.select(value, item); + }); + return; + } + if (!isPresent(value)) { if (!this.validateSelect(this.selectKit.highlighted)) { return; @@ -98,7 +116,7 @@ export default SelectKitComponent.extend({ ); this.selectKit.change( - newValues, + [...new Set(newValues)], newContent.length ? newContent : makeArray(this.defaultItem(value, value)) @@ -131,9 +149,9 @@ export default SelectKitComponent.extend({ }); return this.selectKit.modifySelection(content); - } else { - return this.selectKit.noneItem; } + + return null; }), _onKeydown(event) { @@ -142,7 +160,6 @@ export default SelectKitComponent.extend({ event.target.classList.contains("selected-name") ) { event.stopPropagation(); - this.selectKit.deselectByValue(event.target.dataset.value); return false; } @@ -171,23 +188,4 @@ export default SelectKitComponent.extend({ return true; }, - - handleDeprecations() { - this._super(...arguments); - - this._deprecateValues(); - }, - - _deprecateValues() { - if (this.values && !this.value) { - deprecated( - "The `values` property is deprecated for multi-select. Use `value` instead", - { - since: "v2.4.0", - } - ); - - this.set("value", this.values); - } - }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select/format-selected-content.js b/app/assets/javascripts/select-kit/addon/components/multi-select/format-selected-content.js new file mode 100644 index 00000000000..c2469d737c3 --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/multi-select/format-selected-content.js @@ -0,0 +1,22 @@ +import Component from "@ember/component"; +import { computed } from "@ember/object"; +import layout from "select-kit/templates/components/multi-select/format-selected-content"; +import { makeArray } from "discourse-common/lib/helpers"; +import UtilsMixin from "select-kit/mixins/utils"; + +export default Component.extend(UtilsMixin, { + tagName: "", + layout, + content: null, + selectKit: null, + + formatedContent: computed("content", function () { + if (this.content) { + return makeArray(this.content) + .map((c) => this.getName(c)) + .join(", "); + } else { + return this.getName(this.selectKit.noneItem); + } + }), +}); diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.js b/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.js index ed3b298da8b..72f200b4ba1 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.js +++ b/app/assets/javascripts/select-kit/addon/components/multi-select/multi-select-header.js @@ -1,33 +1,21 @@ import SelectKitHeaderComponent from "select-kit/components/select-kit/select-kit-header"; -import { computed } from "@ember/object"; import layout from "select-kit/templates/components/multi-select/multi-select-header"; -import { makeArray } from "discourse-common/lib/helpers"; +import { computed } from "@ember/object"; +import { reads } from "@ember/object/computed"; export default SelectKitHeaderComponent.extend({ + tagName: "summary", classNames: ["multi-select-header"], layout, - selectedNames: computed("selectedContent", function () { - return makeArray(this.selectedContent).map((c) => this.getName(c)); - }), - - hasReachedMaximumSelection: computed("selectedValue", function () { - if (!this.selectKit.options.maximum) { - return false; + caretUpIcon: reads("selectKit.options.caretUpIcon"), + caretDownIcon: reads("selectKit.options.caretDownIcon"), + caretIcon: computed( + "selectKit.isExpanded", + "caretUpIcon", + "caretDownIcon", + function () { + return this.selectKit.isExpanded ? this.caretUpIcon : this.caretDownIcon; } - - return this.selectedValue.length >= this.selectKit.options.maximum; - }), - - selectedValue: computed("selectedContent", function () { - return makeArray(this.selectedContent) - .map((c) => { - if (this.getName(c) !== this.getName(this.selectKit.noneItem)) { - return this.getValue(c); - } - - return null; - }) - .filter(Boolean); - }), + ), }); 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 3551ce7bb35..d74d48380b1 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -1,3 +1,4 @@ +import { INPUT_DELAY } from "discourse-common/config/environment"; import EmberObject, { computed, get } from "@ember/object"; import PluginApiMixin, { applyContentPluginApiCallbacks, @@ -36,6 +37,7 @@ export default Component.extend( PluginApiMixin, UtilsMixin, { + tagName: "details", pluginApiIdentifiers: ["select-kit"], classNames: ["select-kit"], classNameBindings: [ @@ -75,7 +77,7 @@ export default Component.extend( this.set( "selectKit", EmberObject.create({ - uniqueID: guidFor(this), + uniqueID: this.attrs?.id || guidFor(this), valueProperty: this.valueProperty, nameProperty: this.nameProperty, labelProperty: this.labelProperty, @@ -112,11 +114,16 @@ export default Component.extend( open: bind(this, this._open), highlightNext: bind(this, this._highlightNext), highlightPrevious: bind(this, this._highlightPrevious), + highlightLast: bind(this, this._highlightLast), + highlightFirst: bind(this, this._highlightFirst), change: bind(this, this._onChangeWrapper), select: bind(this, this.select), deselect: bind(this, this.deselect), deselectByValue: bind(this, this.deselectByValue), append: bind(this, this.append), + cancelSearch: bind(this, this._cancelSearch), + triggerSearch: bind(this, this.triggerSearch), + focusFilter: bind(this, this._focusFilter), onOpen: bind(this, this._onOpenWrapper), onClose: bind(this, this._onCloseWrapper), @@ -124,6 +131,10 @@ export default Component.extend( onClearSelection: bind(this, this._onClearSelection), onHover: bind(this, this._onHover), onKeydown: bind(this, this._onKeydownWrapper), + + mainElement: bind(this, this._mainElement), + headerElement: bind(this, this._headerElement), + bodyElement: bind(this, this._bodyElement), }) ); }, @@ -185,15 +196,23 @@ export default Component.extend( this.handleDeprecations(); }, + didInsertElement() { + this._super(...arguments); + + this.element.addEventListener("toggle", this.selectKit.toggle); + }, + willDestroyElement() { this._super(...arguments); - this._searchPromise && cancel(this._searchPromise); + this._cancelSearch(); if (this.popper) { this.popper.destroy(); this.popper = null; } + + this.element.removeEventListener("toggle", this.selectKit.toggle); }, didReceiveAttrs() { @@ -260,7 +279,7 @@ export default Component.extend( filterable: false, autoFilterable: "autoFilterable", filterIcon: "search", - filterPlaceholder: "filterPlaceholder", + filterPlaceholder: null, translatedFilterPlaceholder: null, icon: null, icons: null, @@ -269,13 +288,12 @@ export default Component.extend( minimum: null, minimumLabel: null, autoInsertNoneItem: true, - clearOnClick: false, closeOnChange: true, limitMatches: null, placement: isDocumentRTL() ? "bottom-end" : "bottom-start", - placementStrategy: null, filterComponent: "select-kit/select-kit-filter", selectedNameComponent: "selected-name", + selectedChoiceComponent: "selected-choice", castInteger: false, preventsClickPropagation: false, focusAfterOnChange: true, @@ -291,12 +309,6 @@ export default Component.extend( ); }), - filterPlaceholder: computed("options.allowAny", function () { - return this.options.allowAny - ? "select_kit.filter_placeholder_with_any" - : "select_kit.filter_placeholder"; - }), - collections: computed( "selectedContent.[]", "mainCollection.[]", @@ -386,11 +398,18 @@ export default Component.extend( cancel(this._searchPromise); } - discourseDebounce(this, this._debouncedInput, event.target.value, 200); + this.selectKit.set("isLoading", true); + + discourseDebounce( + this, + this._debouncedInput, + event.target.value, + INPUT_DELAY + ); }, _debouncedInput(filter) { - this.selectKit.setProperties({ filter, isLoading: true }); + this.selectKit.set("filter", filter); this.triggerSearch(filter); }, @@ -435,8 +454,11 @@ export default Component.extend( resolve(items); }).finally(() => { if (!this.isDestroying && !this.isDestroyed) { - if (this.selectKit.options.closeOnChange) { - this.selectKit.close(); + if ( + this.selectKit.options.closeOnChange && + this.selectKit.mainElement() + ) { + this.selectKit.mainElement().open = false; } if (this.selectKit.options.focusAfterOnChange) { @@ -505,6 +527,18 @@ export default Component.extend( return this._boundaryActionHandler("onKeydown", event); }, + _mainElement() { + return document.querySelector(`#${this.selectKit.uniqueID}`); + }, + + _headerElement() { + return this.selectKit.mainElement().querySelector("summary"); + }, + + _bodyElement() { + return this.selectKit.mainElement().querySelector(".select-kit-body"); + }, + _onHover(value, item) { throttle(this, this._highlight, item, 25, true); }, @@ -572,15 +606,18 @@ export default Component.extend( }, triggerSearch(filter) { - if (this._searchPromise) { - cancel(this._searchPromise); - } + this._searchPromise && cancel(this._searchPromise); + this._searchPromise = this._searchWrapper( filter || this.selectKit.filter ); }, _searchWrapper(filter) { + if (this.isDestroyed || this.isDestroying) { + return Promise.resolve([]); + } + this.clearErrors(); this.setProperties({ mainCollection: [], @@ -593,6 +630,10 @@ export default Component.extend( return Promise.resolve(this.search(filter)) .then((result) => { + if (this.isDestroyed || this.isDestroying) { + return []; + } + content = content.concat(makeArray(result)); content = this.selectKit.modifyContent(content).filter(Boolean); @@ -620,7 +661,6 @@ export default Component.extend( } const hasNoContent = isEmpty(content); - if ( this.selectKit.hasSelection && noneItem && @@ -635,6 +675,8 @@ export default Component.extend( highlighted: this.singleSelect && this.value ? this.itemForValue(this.value, this.mainCollection) + : isEmpty(this.selectKit.filter) + ? null : this.mainCollection.firstObject, isLoading: false, hasNoContent, @@ -665,17 +707,29 @@ export default Component.extend( }); }, - _scrollToRow(rowItem) { + _scrollToRow(rowItem, preventScroll = true) { const value = this.getValue(rowItem); const rowContainer = this.element.querySelector( `.select-kit-row[data-value="${value}"]` ); + rowContainer && rowContainer.focus({ preventScroll }); + }, - if (rowContainer) { - const collectionContainer = rowContainer.parentNode; + _highlightLast() { + const highlighted = this.mainCollection.objectAt( + this.mainCollection.length - 1 + ); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + }, - collectionContainer.scrollTop = - rowContainer.offsetTop - collectionContainer.offsetTop; + _highlightFirst() { + const highlighted = this.mainCollection.objectAt(0); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); } }, @@ -688,12 +742,16 @@ export default Component.extend( if (highlightedIndex < count - 1) { highlightedIndex = highlightedIndex + 1; } else { - highlightedIndex = 0; + if (this.selectKit.isFilterExpanded) { + this._focusFilter(); + } else { + highlightedIndex = 0; + } } const highlighted = this.mainCollection.objectAt(highlightedIndex); if (highlighted) { - this._scrollToRow(highlighted); + this._scrollToRow(highlighted, false); this.set("selectKit.highlighted", highlighted); } }, @@ -707,12 +765,16 @@ export default Component.extend( if (highlightedIndex > 0) { highlightedIndex = highlightedIndex - 1; } else { - highlightedIndex = count - 1; + if (this.selectKit.isFilterExpanded) { + this._focusFilter(); + } else { + highlightedIndex = count - 1; + } } const highlighted = this.mainCollection.objectAt(highlightedIndex); if (highlighted) { - this._scrollToRow(highlighted); + this._scrollToRow(highlighted, false); this.set("selectKit.highlighted", highlighted); } }, @@ -747,7 +809,12 @@ export default Component.extend( return this._boundaryActionHandler("onOpen"); }, + _cancelSearch() { + this._searchPromise && cancel(this._searchPromise); + }, + _onCloseWrapper() { + this._cancelSearch(); this.set("selectKit.highlighted", null); return this._boundaryActionHandler("onClose"); @@ -768,6 +835,12 @@ export default Component.extend( this.clearErrors(); + const inModal = this.element.closest("#discourse-modal"); + if (inModal && this.site.mobileView) { + const modalBody = inModal.querySelector(".modal-body"); + modalBody.style = ""; + } + this.selectKit.onClose(event); this.selectKit.setProperties({ @@ -783,6 +856,8 @@ export default Component.extend( this.clearErrors(); + const inModal = this.element.closest("#discourse-modal"); + this.selectKit.onOpen(event); if (!this.popper) { @@ -793,13 +868,7 @@ export default Component.extend( `#${this.selectKit.uniqueID}-body` ); - const inModal = $(this.element).parents("#discourse-modal").length; - - let placementStrategy = this.selectKit.options.placementStrategy; - if (!placementStrategy) { - placementStrategy = inModal ? "fixed" : "absolute"; - } - + const placementStrategy = this.site.mobileView ? "absolute" : "fixed"; const verticalOffset = 3; this.popper = createPopper(anchor, popper, { @@ -855,62 +924,32 @@ export default Component.extend( requires: ["computeStyles"], fn: ({ state }) => { state.styles.popper.minWidth = `${state.rects.reference.width}px`; + + if (state.rects.reference.width >= 300) { + state.styles.popper.maxWidth = `${state.rects.reference.width}px`; + } else { + state.styles.popper.maxWidth = "300px"; + } }, effect: ({ state }) => { state.elements.popper.style.minWidth = `${state.elements.reference.offsetWidth}px`; + + if (state.elements.reference.offsetWidth >= 300) { + state.elements.popper.style.maxWidth = `${state.elements.reference.offsetWidth}px`; + } else { + state.elements.popper.style.maxWidth = "300px"; + } }, }, { - name: "positionWrapper", + name: "modalHeight", + enabled: !!(inModal && this.site.mobileView), phase: "afterWrite", - enabled: true, - fn: (data) => { - const wrapper = this.element.querySelector( - ".select-kit-wrapper" - ); - if (wrapper) { - let height = this.element.offsetHeight + verticalOffset; - - const body = this.element.querySelector(".select-kit-body"); - if (body) { - height += body.offsetHeight; - } - - const popperElement = data.state.elements.popper; - const topPlacement = - popperElement && - popperElement - .getAttribute("data-popper-placement") - .startsWith("top-"); - if (topPlacement) { - this.element.classList.remove("is-under"); - this.element.classList.add("is-above"); - } else { - this.element.classList.remove("is-above"); - this.element.classList.add("is-under"); - } - - wrapper.style.width = `${this.element.offsetWidth}px`; - wrapper.style.height = `${height}px`; - if (placementStrategy === "fixed") { - const rects = this.element.getClientRects()[0]; - - if (rects) { - const bodyRects = body && body.getClientRects()[0]; - - wrapper.style.position = "fixed"; - wrapper.style.left = `${rects.left}px`; - if (topPlacement && bodyRects) { - wrapper.style.top = `${rects.top - bodyRects.height}px`; - } else { - wrapper.style.top = `${rects.top}px`; - } - if (isDocumentRTL()) { - wrapper.style.right = "unset"; - } - } - } - } + fn: ({ state }) => { + const modalBody = inModal.querySelector(".modal-body"); + modalBody.style = ""; + modalBody.style.height = + modalBody.clientHeight + state.rects.popper.height + "px"; }, }, ], @@ -953,6 +992,16 @@ export default Component.extend( }, _focusFilter(forceHeader = false) { + if (!this.selectKit.mainElement()) { + return; + } + + if (!this.selectKit.mainElement().open) { + const headerContainer = this.getHeader(); + headerContainer && headerContainer.focus({ preventScroll: true }); + return; + } + this._safeAfterRender(() => { const input = this.getFilterInput(); if (!forceHeader && input) { diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/errors-collection.js b/app/assets/javascripts/select-kit/addon/components/select-kit/errors-collection.js index f299f38e772..099bd7aeba1 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/errors-collection.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/errors-collection.js @@ -1,11 +1,7 @@ import Component from "@ember/component"; -import { empty } from "@ember/object/computed"; import layout from "select-kit/templates/components/select-kit/errors-collection"; export default Component.extend({ layout, - classNames: ["select-kit-errors-collection"], - classNameBindings: ["shouldHide:hidden"], - tagName: "ul", - shouldHide: empty("collection.content"), + tagName: "", }); diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-body.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-body.js index 5ef8061fd8e..736de1d6f05 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-body.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-body.js @@ -6,14 +6,13 @@ import layout from "select-kit/templates/components/select-kit/select-kit-body"; export default Component.extend({ layout, classNames: ["select-kit-body"], - attributeBindings: ["role"], classNameBindings: ["emptyBody:empty-body"], - emptyBody: computed("selectKit.{filter,hasNoContent}", function () { - return !this.selectKit.filter && this.selectKit.hasNoContent; - }), - rootEventType: "click", - role: "listbox", + emptyBody: computed("selectKit.{filter,hasNoContent}", function () { + return false; + }), + + rootEventType: "click", init() { this._super(...arguments); @@ -24,6 +23,8 @@ export default Component.extend({ didInsertElement() { this._super(...arguments); + this.element.style.position = "relative"; + document.addEventListener( this.rootEventType, this.handleRootMouseDownHandler, @@ -58,6 +59,8 @@ export default Component.extend({ return; } - this.selectKit.close(event); + if (this.selectKit.mainElement()) { + this.selectKit.mainElement().open = false; + } }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-collection.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-collection.js index 1ba82c202ce..8c4b3e1e2a8 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-collection.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-collection.js @@ -1,11 +1,7 @@ import Component from "@ember/component"; -import { empty } from "@ember/object/computed"; import layout from "select-kit/templates/components/select-kit/select-kit-collection"; export default Component.extend({ layout, - classNames: ["select-kit-collection"], - classNameBindings: ["shouldHide:hidden"], - tagName: "ul", - shouldHide: empty("collection"), + tagName: "", }); 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 4e064f5d64d..71eaf00ef5e 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 @@ -1,7 +1,7 @@ import Component from "@ember/component"; import I18n from "I18n"; import UtilsMixin from "select-kit/mixins/utils"; -import { computed } from "@ember/object"; +import { action, computed } from "@ember/object"; import discourseComputed from "discourse-common/utils/decorators"; import { isPresent } from "@ember/utils"; import layout from "select-kit/templates/components/select-kit/select-kit-filter"; @@ -12,8 +12,7 @@ export default Component.extend(UtilsMixin, { classNames: ["select-kit-filter"], classNameBindings: ["isExpanded:is-expanded"], attributeBindings: ["role"], - - role: "searchbox", + tabIndex: -1, isHidden: computed( "selectKit.options.{filterable,allowAny,autoFilterable}", @@ -31,7 +30,8 @@ export default Component.extend(UtilsMixin, { @discourseComputed( "selectKit.options.filterPlaceholder", - "selectKit.options.translatedFilterPlaceholder" + "selectKit.options.translatedFilterPlaceholder", + "selectKit.options.allowAny" ) placeholder(placeholder, translatedPlaceholder) { if (isPresent(translatedPlaceholder)) { @@ -42,87 +42,83 @@ export default Component.extend(UtilsMixin, { return I18n.t(placeholder); } - return ""; + return I18n.t( + this.selectKit.options.allowAny + ? "select_kit.filter_placeholder_with_any" + : "select_kit.filter_placeholder" + ); }, - actions: { - onPaste() {}, + @action + onPaste() {}, - onInput(event) { - this.selectKit.onInput(event); + @action + onInput(event) { + this.selectKit.onInput(event); + return true; + }, + + @action + onKeyup(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + return true; + }, + + @action + onKeydown(event) { + if (!this.selectKit.onKeydown(event)) { + return false; + } + + if (event.key === "Tab" && this.selectKit.isLoading) { + this.selectKit.cancelSearch(); + this.selectKit.mainElement().open = false; return true; - }, + } - onKeyup(event) { - if (event.key === "Enter" && this.selectKit.enterDisabled) { - this.element.querySelector("input").focus(); + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + return true; + } + + if (event.key === "ArrowUp") { + this.selectKit.highlightLast(); + return false; + } + + if (event.key === "ArrowDown") { + this.selectKit.highlightFirst(); + return false; + } + + if (event.key === "Escape") { + this.selectKit.mainElement().open = false; + this.selectKit.headerElement().focus(); + return false; + } + + if (event.key === "Enter" && this.selectKit.highlighted) { + this.selectKit.select( + this.getValue(this.selectKit.highlighted), + this.selectKit.highlighted + ); + event.preventDefault(); + event.stopImmediatePropagation(); + return false; + } + + if ( + event.key === "Enter" && + (!this.selectKit.highlighted || this.selectKit.enterDisabled) + ) { + this.element.querySelector("input").focus(); + if (this.selectKit.enterDisabled) { event.preventDefault(); - event.stopPropagation(); - return false; + event.stopImmediatePropagation(); } - return true; - }, + return false; + } - onKeydown(event) { - if (!this.selectKit.onKeydown(event)) { - return false; - } - - // Do nothing for left/right arrow - if (event.key === "ArrowLeft" || event.key === "ArrowRight") { - return true; - } - - if (event.key === "ArrowUp") { - this.selectKit.highlightPrevious(); - return false; - } - - if (event.key === "ArrowDown") { - this.selectKit.highlightNext(); - return false; - } - - // Escape - if (event.key === "Escape") { - this.selectKit.close(event); - return false; - } - - // Enter - if (event.key === "Enter" && this.selectKit.highlighted) { - this.selectKit.select( - this.getValue(this.selectKit.highlighted), - this.selectKit.highlighted - ); - return false; - } - - if ( - event.key === "Enter" && - (!this.selectKit.highlighted || this.selectKit.enterDisabled) - ) { - this.element.querySelector("input").focus(); - if (this.selectKit.enterDisabled) { - event.preventDefault(); - event.stopPropagation(); - } - return false; - } - - // Tab - if (event.key === "Tab") { - if (this.selectKit.highlighted && this.selectKit.isExpanded) { - this.selectKit.select( - this.getValue(this.selectKit.highlighted), - this.selectKit.highlighted - ); - } - this.selectKit.close(event); - return; - } - - this.selectKit.set("highlighted", null); - }, + this.selectKit.set("highlighted", null); }, }); 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 7e9c361e6c2..624f86e2946 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 @@ -2,38 +2,28 @@ import Component from "@ember/component"; import UtilsMixin from "select-kit/mixins/utils"; import { computed } from "@ember/object"; import { makeArray } from "discourse-common/lib/helpers"; -import { schedule } from "@ember/runloop"; export default Component.extend(UtilsMixin, { - eventType: "click", - - click(event) { - if (typeof document === "undefined") { - return; - } - if (this.isDestroyed || !this.selectKit || this.selectKit.isDisabled) { - return; - } - if (this.eventType !== "click" || event.button !== 0) { - return; - } - this.selectKit.toggle(event); - event.preventDefault(); - }, - classNames: ["select-kit-header"], classNameBindings: ["isFocused"], attributeBindings: [ + "role", "tabindex", - "ariaOwns:aria-owns", - "ariaHasPopup:aria-haspopup", - "ariaIsExpanded:aria-expanded", - "headerRole:role", + "ariaLevel:aria-level", "selectedValue:data-value", "selectedNames:data-name", "buttonTitle:title", + "selectKit.options.autofocus:autofocus", ], + selectKit: null, + + role: "application", + + ariaLevel: 1, + + tabindex: 0, + selectedValue: computed("value", function () { return this.value === this.getValue(this.selectKit.noneItem) ? null @@ -62,20 +52,6 @@ export default Component.extend(UtilsMixin, { return icon.concat(icons).filter(Boolean); }), - ariaIsExpanded: computed("selectKit.isExpanded", function () { - return this.selectKit.isExpanded ? "true" : "false"; - }), - - ariaHasPopup: "menu", - - ariaOwns: computed("selectKit.uniqueID", function () { - return `${this.selectKit.uniqueID}-body`; - }), - - headerRole: "listbox", - - tabindex: 0, - didInsertElement() { this._super(...arguments); if (this.selectKit.options.autofocus) { @@ -83,6 +59,10 @@ export default Component.extend(UtilsMixin, { } }, + click(event) { + event.stopImmediatePropagation(); + }, + keyUp(event) { if (event.key === " ") { event.preventDefault(); @@ -104,6 +84,8 @@ export default Component.extend(UtilsMixin, { } if (event.key === "Enter") { + event.stopPropagation(); + if (this.selectKit.isExpanded) { if (this.selectKit.highlighted) { this.selectKit.select( @@ -113,44 +95,40 @@ export default Component.extend(UtilsMixin, { return false; } } else { - this.selectKit.close(event); + this.selectKit.mainElement().open = false; } } else if (event.key === "ArrowUp") { + event.stopPropagation(); + if (this.selectKit.isExpanded) { this.selectKit.highlightPrevious(); } else { - this.selectKit.open(event); + this.selectKit.mainElement().open = true; } return false; } else if (event.key === "ArrowDown") { + event.stopPropagation(); if (this.selectKit.isExpanded) { this.selectKit.highlightNext(); } else { - this.selectKit.open(event); + this.selectKit.mainElement().open = true; } return false; - } else if (event.key === "ArrowLeft" || event.key === "ArrowRight") { - // Do nothing for left/right arrow - return true; } else if (event.key === " ") { + event.stopPropagation(); event.preventDefault(); // prevents the space to trigger a scroll page-next - this.selectKit.toggle(event); + this.selectKit.mainElement().open = true; } else if (event.key === "Escape") { - this.selectKit.close(event); + event.stopPropagation(); + if (this.selectKit.isExpanded) { + this.selectKit.mainElement().open = false; + } else { + this.element.blur(); + } + } else if (event.key === "Tab") { + return true; } else if (event.key === "Backspace") { this._focusFilterInput(); - } else if (event.key === "Tab") { - if ( - this.selectKit.highlighted && - this.selectKit.isExpanded && - this.selectKit.options.triggerOnChangeOnTab - ) { - this.selectKit.select( - this.getValue(this.selectKit.highlighted), - this.selectKit.highlighted - ); - } - this.selectKit.close(event); } else if ( this.selectKit.options.filterable || this.selectKit.options.autoFilterable || @@ -159,8 +137,12 @@ export default Component.extend(UtilsMixin, { if (this.selectKit.isExpanded) { this._focusFilterInput(); } else { - this.selectKit.open(event); - schedule("afterRender", () => this._focusFilterInput()); + if (this.isValidInput(event.key)) { + this.selectKit.set("filter", event.key); + this.selectKit.mainElement().open = true; + event.preventDefault(); + event.stopPropagation(); + } } } else { if (this.selectKit.isExpanded) { diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js index 9e8b734e558..cf7d5ac90aa 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js @@ -1,3 +1,4 @@ +import { propertyEqual } from "discourse/lib/computed"; import { action, computed } from "@ember/object"; import Component from "@ember/component"; import I18n from "I18n"; @@ -11,17 +12,16 @@ export default Component.extend(UtilsMixin, { layout, classNames: ["select-kit-row"], tagName: "li", - tabIndex: -1, + tabIndex: 0, attributeBindings: [ "tabIndex", "title", "rowValue:data-value", "rowName:data-name", - "ariaLabel:aria-label", - "ariaSelected:aria-selected", + "role", + "ariaChecked:aria-checked", "guid:data-guid", "rowLang:lang", - "role", ], classNameBindings: [ "isHighlighted", @@ -31,15 +31,24 @@ export default Component.extend(UtilsMixin, { "item.classNames", ], + role: "menuitemradio", + didInsertElement() { this._super(...arguments); - this.element.addEventListener("mouseenter", this.handleMouseEnter); + + if (!this.site.mobileView) { + this.element.addEventListener("mouseenter", this.handleMouseEnter); + this.element.addEventListener("focus", this.handleMouseEnter); + this.element.addEventListener("blur", this.handleBlur); + } }, willDestroyElement() { this._super(...arguments); - if (this.element) { - this.element.removeEventListener("mouseenter", this.handleMouseEnter); + if (!this.site.mobileView && this.element) { + this.element.removeEventListener("mouseenter", this.handleBlur); + this.element.removeEventListener("focus", this.handleMouseEnter); + this.element.removeEventListener("blur", this.handleMouseEnter); } }, @@ -47,19 +56,13 @@ export default Component.extend(UtilsMixin, { return this.rowValue === this.getValue(this.selectKit.noneItem); }), - role: "option", - guid: computed("item", function () { return guidFor(this.item); }), lang: reads("item.lang"), - ariaLabel: computed("item.ariaLabel", "title", function () { - return this.getProperty(this.item, "ariaLabel") || this.title; - }), - - ariaSelected: computed("isSelected", function () { + ariaChecked: computed("isSelected", function () { return this.isSelected ? "true" : "false"; }), @@ -112,23 +115,36 @@ export default Component.extend(UtilsMixin, { return this.getValue(this.selectKit.highlighted); }), - isHighlighted: computed("rowValue", "highlightedValue", function () { - return this.rowValue === this.highlightedValue; - }), + isHighlighted: propertyEqual("rowValue", "highlightedValue"), - isSelected: computed("rowValue", "value", function () { - return this.rowValue === this.value; - }), + isSelected: propertyEqual("rowValue", "value"), @action handleMouseEnter() { if (!this.isDestroying || !this.isDestroyed) { + this.element.focus({ preventScroll: true }); this.selectKit.onHover(this.rowValue, this.item); } return false; }, - click() { + @action + handleBlur(event) { + if ( + (!this.isDestroying || !this.isDestroyed) && + event.relatedTarget && + this.selectKit.mainElement() + ) { + if (!this.selectKit.mainElement().contains(event.relatedTarget)) { + this.selectKit.mainElement().open = false; + } + } + return false; + }, + + click(event) { + event.preventDefault(); + event.stopPropagation(); this.selectKit.select(this.rowValue, this.item); return false; }, @@ -138,4 +154,44 @@ export default Component.extend(UtilsMixin, { event.preventDefault(); } }, + + keyDown(event) { + if (this.selectKit.isExpanded) { + if (event.key === "Backspace") { + if (this.selectKit.isFilterExpanded) { + this.selectKit.set("filter", this.selectKit.filter.slice(0, -1)); + this.selectKit.triggerSearch(); + this.selectKit.focusFilter(); + event.preventDefault(); + event.stopPropagation(); + return false; + } + } else if (event.key === "ArrowUp") { + this.selectKit.highlightPrevious(); + return false; + } else if (event.key === "ArrowDown") { + this.selectKit.highlightNext(); + return false; + } else if (event.key === "Enter") { + event.stopImmediatePropagation(); + + this.selectKit.select( + this.getValue(this.selectKit.highlighted), + this.selectKit.highlighted + ); + return false; + } else if (event.key === "Escape") { + this.selectKit.mainElement().open = false; + this.selectKit.headerElement().focus(); + } else { + if (this.isValidInput(event.key)) { + this.selectKit.set("filter", event.key); + this.selectKit.triggerSearch(); + this.selectKit.focusFilter(); + event.preventDefault(); + event.stopPropagation(); + } + } + } + }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/single-select-header.js b/app/assets/javascripts/select-kit/addon/components/select-kit/single-select-header.js index 337f25de5ec..168e5b9e867 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/single-select-header.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/single-select-header.js @@ -5,11 +5,18 @@ import layout from "select-kit/templates/components/select-kit/single-select-hea import I18n from "I18n"; export default SelectKitHeaderComponent.extend(UtilsMixin, { + tagName: "summary", layout, classNames: ["single-select-header"], - attributeBindings: ["role", "name"], + attributeBindings: ["name"], - role: "combobox", + focusIn(event) { + document.querySelectorAll(".select-kit-header").forEach((header) => { + if (header !== event.target) { + header.parentNode.open = false; + } + }); + }, name: computed("selectedContent.name", function () { if (this.selectedContent) { @@ -20,10 +27,4 @@ export default SelectKitHeaderComponent.extend(UtilsMixin, { return I18n.t("select_kit.select_to_filter"); } }), - - mouseDown(event) { - if (this.selectKit.options.preventHeaderFocus) { - event.preventDefault(); - } - }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/selected-choice-category.js b/app/assets/javascripts/select-kit/addon/components/selected-choice-category.js new file mode 100644 index 00000000000..9fdb94831a2 --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/selected-choice-category.js @@ -0,0 +1,17 @@ +import layout from "select-kit/templates/components/selected-choice-category"; +import SelectedChoiceComponent from "select-kit/components/selected-choice"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; +import { computed } from "@ember/object"; + +export default SelectedChoiceComponent.extend({ + tagName: "", + layout, + extraClass: "selected-choice-category", + + badge: computed("item", function () { + return categoryBadgeHTML(this.item, { + allowUncategorized: true, + link: false, + }).htmlSafe(); + }), +}); diff --git a/app/assets/javascripts/select-kit/addon/components/selected-choice-color.js b/app/assets/javascripts/select-kit/addon/components/selected-choice-color.js new file mode 100644 index 00000000000..7d9cc14e3ce --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/selected-choice-color.js @@ -0,0 +1,31 @@ +import { escapeExpression } from "discourse/lib/utilities"; +import SelectedChoiceComponent from "select-kit/components/selected-choice"; +import { schedule } from "@ember/runloop"; +import { computed } from "@ember/object"; + +export default SelectedChoiceComponent.extend({ + tagName: "", + + extraClass: "selected-choice-color", + + escapedColor: computed("item", function () { + const color = `${escapeExpression(this.item?.name || this.item)}`; + return color.startsWith("#") ? color : `#${color}`; + }), + + didInsertElement() { + this._super(...arguments); + + schedule("afterRender", () => { + const element = document.querySelector( + `#${this.selectKit.uniqueID} #${this.id}-choice` + ); + + if (!element) { + return; + } + + element.style.borderBottomColor = this.escapedColor; + }); + }, +}); diff --git a/app/assets/javascripts/select-kit/addon/components/selected-choice.js b/app/assets/javascripts/select-kit/addon/components/selected-choice.js new file mode 100644 index 00000000000..1768c202fd7 --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/selected-choice.js @@ -0,0 +1,28 @@ +import { guidFor } from "@ember/object/internals"; +import Component from "@ember/component"; +import { computed } from "@ember/object"; +import layout from "select-kit/templates/components/selected-choice"; +import UtilsMixin from "select-kit/mixins/utils"; + +export default Component.extend(UtilsMixin, { + tagName: "", + layout, + item: null, + selectKit: null, + extraClass: null, + id: null, + + init() { + this._super(...arguments); + + this.set("id", guidFor(this)); + }, + + itemValue: computed("item", function () { + return this.getValue(this.item); + }), + + itemName: computed("item", function () { + return this.getName(this.item); + }), +}); diff --git a/app/assets/javascripts/select-kit/addon/components/selected-color.js b/app/assets/javascripts/select-kit/addon/components/selected-color.js index 9b4cbe27b24..12b52eecac6 100644 --- a/app/assets/javascripts/select-kit/addon/components/selected-color.js +++ b/app/assets/javascripts/select-kit/addon/components/selected-color.js @@ -9,13 +9,17 @@ export default SelectedNameComponent.extend({ this._super(...arguments); schedule("afterRender", () => { - const color = escapeExpression(this.name), - el = document.querySelector(`[data-value="${color}"]`); + const element = document.querySelector( + `#${this.selectKit.uniqueID} #${this.id}` + ); - if (el) { - el.style.borderBottom = "2px solid transparent"; - el.style.borderBottomColor = `#${color}`; + if (!element) { + return; } + + element.style.borderBottom = "2px solid transparent"; + const color = escapeExpression(this.name); + element.style.borderBottomColor = `#${color}`; }); }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/selected-name.js b/app/assets/javascripts/select-kit/addon/components/selected-name.js index 0ec2da4077f..aad9ebb5646 100644 --- a/app/assets/javascripts/select-kit/addon/components/selected-name.js +++ b/app/assets/javascripts/select-kit/addon/components/selected-name.js @@ -1,4 +1,5 @@ -import { action, computed, get } from "@ember/object"; +import { guidFor } from "@ember/object/internals"; +import { computed, get } from "@ember/object"; import Component from "@ember/component"; import UtilsMixin from "select-kit/mixins/utils"; import layout from "select-kit/templates/components/selected-name"; @@ -13,13 +14,12 @@ export default Component.extend(UtilsMixin, { headerTitle: null, headerLang: null, headerLabel: null, + id: null, - @action - onSelectedNameClick() { - if (this.selectKit.options.clearOnClick) { - this.selectKit.deselect(this.item); - return false; - } + init() { + this._super(...arguments); + + this.set("id", guidFor(this)); }, didReceiveAttrs() { diff --git a/app/assets/javascripts/select-kit/addon/components/tag-chooser.js b/app/assets/javascripts/select-kit/addon/components/tag-chooser.js index 64693c62fea..e7518f48626 100644 --- a/app/assets/javascripts/select-kit/addon/components/tag-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/tag-chooser.js @@ -105,6 +105,10 @@ export default MultiSelectComponent.extend(TagsMixin, { }, _transformJson(context, json) { + if (context.isDestroyed || context.isDestroying) { + return []; + } + let results = json.results; context.setProperties({ diff --git a/app/assets/javascripts/select-kit/addon/components/tag-drop.js b/app/assets/javascripts/select-kit/addon/components/tag-drop.js index 7ef59011c44..9bd4bc5a019 100644 --- a/app/assets/javascripts/select-kit/addon/components/tag-drop.js +++ b/app/assets/javascripts/select-kit/addon/components/tag-drop.js @@ -18,7 +18,6 @@ export default ComboBoxComponent.extend(TagsMixin, { classNameBindings: ["categoryStyle", "tagClass"], classNames: ["tag-drop"], value: readOnly("tagId"), - tagName: "li", categoryStyle: setting("category_style"), maxTagSearchResults: setting("max_tag_search_results"), sortTagsAlphabetically: setting("tags_sort_alphabetically"), diff --git a/app/assets/javascripts/select-kit/addon/mixins/utils.js b/app/assets/javascripts/select-kit/addon/mixins/utils.js index cb1c4244bc3..12b92b743d0 100644 --- a/app/assets/javascripts/select-kit/addon/mixins/utils.js +++ b/app/assets/javascripts/select-kit/addon/mixins/utils.js @@ -2,6 +2,14 @@ import Mixin from "@ember/object/mixin"; import { get } from "@ember/object"; export default Mixin.create({ + isValidInput(eventKey) { + // relying on passing the event to the input is risky as it could not work + // dispatching the event won't work as the event won't be trusted + // safest solution is to filter event and prefill filter with it + const nonInputKeysRegex = /F\d+|Arrow.+|Meta|Alt|Control|Shift|Delete|Enter|Escape|Tab|Space|Insert|Backspace/; + return !nonInputKeysRegex.test(eventKey); + }, + defaultItem(value, name) { if (this.selectKit.valueProperty) { const item = {}; diff --git a/app/assets/javascripts/select-kit/addon/templates/components/category-drop/category-drop-header.hbs b/app/assets/javascripts/select-kit/addon/templates/components/category-drop/category-drop-header.hbs index a800565e6d1..55c109ac10f 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/category-drop/category-drop-header.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/category-drop/category-drop-header.hbs @@ -1,8 +1,10 @@ -{{component selectKit.options.selectedNameComponent - tabindex=tabindex - item=selectedContent - selectKit=selectKit - shouldDisplayClearableButton=shouldDisplayClearableButton -}} +