DEV: select-kit third major update with focus on accessibility (#13303)

Major changes included:
- better support for screen readers
- trapping focus in modals
- better tabbing order in composer
- alerts on no content found/number of items found
- better autofocus in modals
- mini-tag-chooser is now a multi-select component
- each multi-select-component will now display selection on one row
This commit is contained in:
Joffrey JAFFEUX 2021-08-23 10:44:19 +02:00 committed by GitHub
parent f1701764a6
commit cb59681d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 1757 additions and 1917 deletions

View File

@ -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
)
}}

View File

@ -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}}

View File

@ -21,7 +21,9 @@
{{/if}}
{{combo-box
allowAny=true
options=(hash
allowAny=true
)
none=noneKey
valueProperty=null
nameProperty=null

View File

@ -682,6 +682,7 @@ export default Component.extend(ComposerUpload, {
extraButtons(toolbar) {
toolbar.addButton({
tabindex: "0",
id: "quote",
group: "fontStyles",
icon: "far-comment",

View File

@ -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",
});

View File

@ -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();
}
},

View File

@ -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;

View File

@ -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) {

View File

@ -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"
)
);
},

View File

@ -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();
}
}
},

View File

@ -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);
}
},
});

View File

@ -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));
}

View File

@ -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
)
}}
<li>
{{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
)
}}
</li>
{{/if}}
{{/each}}
{{#if showTagsSection}}
{{#if additionalTags}}
{{tags-intersection-chooser
currentCategory=category
mainTag=tag.id
additionalTags=additionalTags
options=(hash
categoryId=category.id
)
}}
<li>
{{tags-intersection-chooser
currentCategory=category
mainTag=tag.id
additionalTags=additionalTags
options=(hash
categoryId=category.id
)
}}
</li>
{{else}}
{{tag-drop
currentCategory=category
noSubcategories=noSubcategories
tagId=tag.id
}}
<li>
{{tag-drop
currentCategory=category
noSubcategories=noSubcategories
tagId=tag.id
}}
</li>
{{/if}}
{{/if}}

View File

@ -1,5 +1,4 @@
{{d-editor
tabindex="4"
value=composer.reply
placeholder=replyPlaceholder
previewUpdated=(action "previewUpdated")
@ -18,7 +17,8 @@
onPopupMenuAction=onPopupMenuAction
popupMenuOptions=popupMenuOptions
disabled=disableTextarea
outletArgs=(hash composer=composer editorType="composer")}}
outletArgs=(hash composer=composer editorType="composer")
}}
{{#if allowUpload}}
{{#if acceptsAllFormats}}

View File

@ -1,10 +1,11 @@
{{text-field value=composer.title
tabindex="2"
id="reply-title"
maxLength=titleMaxLength
placeholderKey=composer.titlePlaceholder
aria-label=(I18n composer.titlePlaceholder)
disabled=disabled
autocomplete="discourse"}}
{{text-field
value=composer.title
id="reply-title"
maxLength=titleMaxLength
placeholderKey=composer.titlePlaceholder
aria-label=(I18n composer.titlePlaceholder)
disabled=disabled
autocomplete="discourse"
}}
{{popup-input-tip validation=validation}}

View File

@ -9,6 +9,7 @@
title=toggleToolbarTitle
ariaLabel=toggleToolbarTitle
preventFocus=true
tabindex=-1
}}
{{/if}}
@ -18,6 +19,7 @@
action=toggleComposer
title=toggleTitle
ariaLabel=toggleTitle
tabindex=-1
}}
{{#unless site.mobileView}}
@ -27,6 +29,7 @@
action=toggleFullscreen
title=fullscreenTitle
ariaLabel=fullscreenTitle
tabindex=-1
}}
{{/unless}}
</div>

View File

@ -1,11 +1,10 @@
{{email-group-user-chooser
id="private-message-users"
tabindex="1"
value=splitRecipients
onChange=(action "updateRecipients")
options=(hash
topicId=topicId
filterPlaceholder="composer.users_placeholder"
none="composer.users_placeholder"
includeMessageableGroups=true
allowEmails=currentUser.can_send_private_email_messages
autoWrap=true

View File

@ -1,6 +1,6 @@
<div class="d-editor-container">
<div class="d-editor-textarea-wrapper {{if disabled "disabled"}} {{if isEditorFocused "in-focus"}}">
<div class="d-editor-button-bar">
<div class="d-editor-button-bar" role="toolbar">
{{#each toolbar.groups as |group|}}
{{#each group.buttons as |b|}}
{{#if b.popupMenu}}
@ -9,6 +9,8 @@
onChange=onPopupMenuAction
onOpen=(action b.action b)
class=b.className
tabindex=-1
onKeydown=rovingButtonBar
options=(hash
icon=b.icon
focusAfterOnChange=false
@ -24,6 +26,8 @@
icon=b.icon
class=b.className
preventFocus=b.preventFocus
tabindex=b.tabindex
onKeyDown=rovingButtonBar
}}
{{/if}}
{{/each}}
@ -45,7 +49,8 @@
disabled=disabled
input=change
focusIn=(action "focusIn")
focusOut=(action "focusOut")}}
focusOut=(action "focusOut")
}}
{{popup-input-tip validation=validation}}
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
</div>

View File

@ -4,10 +4,8 @@
{{#if displayLabelIcon}}{{d-icon displayLabelIcon}}{{/if}}{{displayLabel}}
</label>
{{future-date-input-selector
minimumResultsForSearch=-1
statusType=statusType
value=(readonly selection)
input=(readonly input)
includeDateTime=includeDateTime
includeWeekend=includeWeekend
includeFarFuture=includeFarFuture
@ -26,15 +24,22 @@
<div class="control-group">
{{d-icon "calendar-alt"}}
{{date-picker-future
value=date
defaultDate=date
onSelect=(action (mut date))
value=_date
defaultDate=_date
onSelect=(action "onChangeDate")
}}
</div>
<div class="control-group">
{{d-icon "far-clock"}}
{{input placeholder="--:--" type="time" class="time-input" value=time disabled=timeInputDisabled}}
{{input
placeholder="--:--"
type="time"
class="time-input"
value=_time
disabled=timeInputDisabled
input=(action "onChangeTime" value="target.value")
}}
</div>
{{/if}}
</div>

View File

@ -6,10 +6,12 @@
{{/link-to}}
</li>
{{else}}
{{group-dropdown
groups=group.extras.visible_group_names
value=group.name
}}
<li>
{{group-dropdown
groups=group.extras.visible_group_names
value=group.name
}}
</li>
{{/if}}
{{#each tabs as |tab|}}

View File

@ -6,9 +6,11 @@
save=(action "save")}}
<div class="grippie"></div>
{{#if visible}}
{{composer-messages composer=model
messageCount=messageCount
addLinkLookup=(action "addLinkLookup")}}
{{composer-messages
composer=model
messageCount=messageCount
addLinkLookup=(action "addLinkLookup")
}}
{{#if model.viewOpenOrFullscreen}}
<div role="form" aria-label={{I18n saveLabel}} class="reply-area {{if canEditTags "with-tags"}}">
<div class="composer-fields">
@ -21,7 +23,7 @@
openComposer=(action "openComposer")
closeComposer=(action "closeComposer")
canWhisper=canWhisper
tabindex=8}}
}}
{{plugin-outlet name="composer-action-after" noTags=true args=(hash model=model)}}
{{#unless site.mobileView}}
@ -37,15 +39,17 @@
{{#if canEdit}}
{{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason icon="info-circle" class="display-edit-reason"}}
{{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}}
{{text-field value=editReason id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}}
{{/link-to-input}}
{{/if}}
</div>
{{/unless}}
{{composer-toggles composeState=model.composeState showToolbar=showToolbar
toggleComposer=(action "toggle")
toggleToolbar=(action "toggleToolbar")
toggleFullscreen=(action "fullscreenComposer")}}
{{composer-toggles
composeState=model.composeState showToolbar=showToolbar
toggleComposer=(action "toggle")
toggleToolbar=(action "toggleToolbar")
toggleFullscreen=(action "fullscreenComposer")
}}
</div>
{{#unless model.viewFullscreen}}
{{#if model.canEditTitle}}
@ -60,7 +64,7 @@
}}
{{#if showWarning}}
<label class="add-warning">
{{input type="checkbox" checked=model.isWarning tabindex="3"}}
{{input type="checkbox" checked=model.isWarning}}
{{i18n "composer.add_warning"}}
</label>
{{/if}}
@ -75,7 +79,6 @@
<div class="category-input">
{{category-chooser
value=model.categoryId
tabindex="3"
onChange=(action (mut model.categoryId))
isDisabled=disableCategoryChooser
options=(hash
@ -89,7 +92,6 @@
{{#if canEditTags}}
{{mini-tag-chooser
value=model.tags
tabindex=4
isDisabled=disableTagsChooser
onChange=(action (mut model.tags))
options=(hash
@ -139,14 +141,16 @@
<div class="save-or-cancel">
{{#unless model.viewFullscreen}}
{{composer-save-button action=(action "save")
icon=saveIcon
label=saveLabel
forwardEvent=true
disableSubmit=disableSubmit}}
{{composer-save-button
action=(action "save")
icon=saveIcon
label=saveLabel
forwardEvent=true
disableSubmit=disableSubmit
}}
{{#if site.mobileView}}
<a href {{action "cancel"}} tabindex="6" title={{i18n "cancel"}} class="cancel">
<a href {{action "cancel"}} title={{i18n "cancel"}} class="cancel">
{{#if canEdit}}
{{d-icon "times"}}
{{else}}
@ -154,7 +158,7 @@
{{/if}}
</a>
{{else}}
<a href {{action "cancel"}} tabindex="6" class="cancel" >{{i18n "cancel"}}</a>
<a href {{action "cancel"}} class="cancel" >{{i18n "cancel"}}</a>
{{/if}}
{{/unless}}
@ -247,8 +251,8 @@
{{composer-toggles composeState=model.composeState
toggleFullscreen=(action "openIfDraft")
toggleComposer=(action "toggle")
toggleToolbar=(action "toggleToolbar")}}
toggleToolbar=(action "toggleToolbar")
}}
{{/if}}
{{/if}}

View File

@ -8,7 +8,7 @@ acceptance("Admin - Search Log Term", function (needs) {
test("show search log term details", async function (assert) {
await visit("/admin/logs/search_logs/term?term=ruby");
assert.ok(exists("div.search-logs-filter"), "has the search type filter");
assert.ok(exists(".search-logs-filter"), "has the search type filter");
assert.ok(exists("canvas.chartjs-render-monitor"), "has graph canvas");
assert.ok(exists("div.header-search-results"), "has header search results");
});

View File

@ -18,7 +18,7 @@ acceptance("Admin - Search Logs", function (needs) {
await click(".term a");
assert.ok(
exists("div.search-logs-filter"),
exists(".search-logs-filter"),
"it should show the search log term page"
);
});

View File

@ -1,7 +1,6 @@
import {
acceptance,
fakeTime,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { click, visit } from "@ember/test-helpers";
@ -26,12 +25,6 @@ acceptance("Admin - Silence User", function (needs) {
await click(".silence-user");
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()

View File

@ -120,12 +120,6 @@ acceptance("Admin - Suspend User - timeframe choosing", function (needs) {
await click(".suspend-user");
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()

View File

@ -23,7 +23,6 @@ acceptance("Composer Actions", function (needs) {
test("creating new topic and then reply_as_private_message keeps attributes", async function (assert) {
await visit("/");
await click("button#create-topic");
await fillIn("#reply-title", "this is the title");
await fillIn(".d-editor-input", "this is the reply");
@ -62,12 +61,8 @@ acceptance("Composer Actions", function (needs) {
await composerActions.expand();
await composerActions.selectRowByValue("reply_as_private_message");
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");
assert.ok(
queryAll(".d-editor-input").val().indexOf("Continuing the discussion") >=
0
@ -182,12 +177,8 @@ acceptance("Composer Actions", function (needs) {
await composerActions.expand();
await composerActions.selectRowByValue("reply_as_new_group_message");
const items = [];
queryAll("#private-message-users .selected-name").each((_, item) =>
items.push(item.textContent.trim())
);
assert.deepEqual(items, ["foo", "foo_group"]);
const privateMessageUsers = selectKit("#private-message-users");
assert.deepEqual(privateMessageUsers.header().value(), "foo,foo_group");
});
test("hide component if no content", async function (assert) {
@ -422,12 +413,8 @@ acceptance("Composer Actions", function (needs) {
await composerActions.expand();
await composerActions.selectRowByValue("reply_as_private_message");
assert.equal(
queryAll("#private-message-users .selected-name:nth-of-type(1)")
.text()
.trim(),
"uwe_keim"
);
const privateMessageUsers = selectKit("#private-message-users");
assert.equal(privateMessageUsers.header().value(), "uwe_keim");
assert.ok(
queryAll(".d-editor-input").val().indexOf("Continuing the discussion") >=
0

View File

@ -27,7 +27,7 @@ async function writeInComposer(assert) {
assert.equal(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a href="/404">test</a></p>'
'<p><a href="/404" tabindex="-1">test</a></p>'
);
await fillIn(".d-editor-input", "[test|attachment](upload://asdsad.png)");
@ -41,7 +41,7 @@ acceptance("Composer Attachment - Cooking", function (needs) {
await writeInComposer(assert);
assert.equal(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a class="attachment" href="/uploads/short-url/asdsad.png">test</a></p>'
'<p><a class="attachment" href="/uploads/short-url/asdsad.png" tabindex="-1">test</a></p>'
);
});
});
@ -55,7 +55,7 @@ acceptance("Composer Attachment - Secure Media Enabled", function (needs) {
await writeInComposer(assert);
assert.equal(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a class="attachment" href="/secure-media-uploads/default/3X/1/asjdiasjdiasida.png">test</a></p>'
'<p><a class="attachment" href="/secure-media-uploads/default/3X/1/asjdiasjdiasida.png" tabindex="-1">test</a></p>'
);
});
});

View File

@ -32,10 +32,10 @@ http://www.example.com/has-title.html
queryAll(".d-editor-preview:visible").html().trim(),
`
<p><aside class=\"onebox\"><article class=\"onebox-body\"><h3><a href=\"http://www.example.com/article.html\">An interesting article</a></h3></article></aside><br>
This is another test <a href=\"http://www.example.com/has-title.html\" class=\"inline-onebox\">This is a great title</a></p>
<p><a href=\"http://www.example.com/no-title.html\" class=\"onebox\" target=\"_blank\">http://www.example.com/no-title.html</a></p>
<p>This is another test <a href=\"http://www.example.com/no-title.html\" class=\"\">http://www.example.com/no-title.html</a><br>
This is another test <a href=\"http://www.example.com/has-title.html\" class=\"inline-onebox\">This is a great title</a></p>
This is another test <a href=\"http://www.example.com/has-title.html\" class=\"inline-onebox\" tabindex=\"-1\">This is a great title</a></p>
<p><a href=\"http://www.example.com/no-title.html\" class=\"onebox\" target=\"_blank\" tabindex=\"-1\">http://www.example.com/no-title.html</a></p>
<p>This is another test <a href=\"http://www.example.com/no-title.html\" class=\"\" tabindex=\"-1\">http://www.example.com/no-title.html</a><br>
This is another test <a href=\"http://www.example.com/has-title.html\" class=\"inline-onebox\" tabindex=\"-1\">This is a great title</a></p>
<p><aside class=\"onebox\"><article class=\"onebox-body\"><h3><a href=\"http://www.example.com/article.html\">An interesting article</a></h3></article></aside></p>
`.trim()
);
@ -69,14 +69,14 @@ acceptance("Composer - Inline Onebox", function (needs) {
assert.equal(requestsCount, 1);
assert.equal(
queryAll(".d-editor-preview").html().trim(),
'<p>Test <a href="http://www.example.com/page" class="inline-onebox-loading">www.example.com/page</a></p>'
'<p>Test <a href="http://www.example.com/page" class="inline-onebox-loading" tabindex="-1">www.example.com/page</a></p>'
);
await fillIn(".d-editor-input", `Test www.example.com/page Test`);
assert.equal(requestsCount, 1);
assert.equal(
queryAll(".d-editor-preview").html().trim(),
'<p>Test <a href="http://www.example.com/page">www.example.com/page</a> Test</p>'
'<p>Test <a href="http://www.example.com/page" tabindex="-1">www.example.com/page</a> Test</p>'
);
});
});

View File

@ -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);
}

View File

@ -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()

View File

@ -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");
});

View File

@ -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"
);

View File

@ -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"

View File

@ -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"
);

View File

@ -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");

View File

@ -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()

View File

@ -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);
});

View File

@ -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"]
);
});

View File

@ -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()

View File

@ -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"
);

View File

@ -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"
);
});

View File

@ -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");

View File

@ -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()

View File

@ -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() {

View File

@ -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
)
);
}
}
);

View File

@ -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");

View File

@ -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'
);
},

View File

@ -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"
);
},
});
}
);

View File

@ -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()
)
);
}
}
);

View File

@ -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(

View File

@ -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");
},
});

View File

@ -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"),

View File

@ -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();
});
});

View File

@ -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,

View File

@ -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"),

View File

@ -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";
},

View File

@ -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}`;
});
},
});

View File

@ -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 () {

View File

@ -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;
}
}
),
});

View File

@ -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,

View File

@ -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}`),

View File

@ -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) {

View File

@ -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),

View File

@ -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";
}
}),

View File

@ -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);
}
}
},
});

View File

@ -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",
};
});
}),
});

View File

@ -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);
}
},
});

View File

@ -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);
}
}),
});

View File

@ -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);
}),
),
});

View File

@ -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) {

View File

@ -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: "",
});

View File

@ -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;
}
},
});

View File

@ -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: "",
});

View File

@ -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);
},
});

View File

@ -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) {

View File

@ -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();
}
}
}
},
});

View File

@ -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();
}
},
});

View File

@ -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();
}),
});

View File

@ -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;
});
},
});

View File

@ -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);
}),
});

View File

@ -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}`;
});
},
});

View File

@ -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() {

View File

@ -105,6 +105,10 @@ export default MultiSelectComponent.extend(TagsMixin, {
},
_transformJson(context, json) {
if (context.isDestroyed || context.isDestroying) {
return [];
}
let results = json.results;
context.setProperties({

View File

@ -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"),

View File

@ -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 = {};

View File

@ -1,8 +1,10 @@
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
shouldDisplayClearableButton=shouldDisplayClearableButton
}}
<div class="select-kit-header-wrapper">
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
shouldDisplayClearableButton=shouldDisplayClearableButton
}}
{{d-icon caretIcon class="caret-icon"}}
{{d-icon caretIcon class="caret-icon"}}
</div>

View File

@ -1,5 +1,5 @@
{{#if category}}
<div class="category-status">
<div class="category-status" aria-hidden="true">
{{#if hasParentCategory}}
{{#unless hideParentCategory}}
{{badgeForParentCategory}}
@ -9,7 +9,7 @@
</div>
{{#if shouldDisplayDescription}}
<div class="category-desc">{{dir-span description}}</div>
<div class="category-desc" aria-hidden="true">{{dir-span description}}</div>
{{/if}}
{{else}}
{{html-safe label}}

View File

@ -1,18 +1,20 @@
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
<div class="select-kit-header-wrapper">
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
}}
{{#if shouldDisplayClearableButton}}
{{d-button
class="btn-clear"
icon="times"
action=selectKit.onClearSelection
ariaLabel="clear_input"
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
}}
{{/if}}
{{d-icon caretIcon class="caret-icon"}}
{{#if shouldDisplayClearableButton}}
{{d-button
class="btn-clear"
icon="times"
action=selectKit.onClearSelection
ariaLabel="clear_input"
}}
{{/if}}
{{d-icon caretIcon class="caret-icon"}}
</div>

View File

@ -1,12 +1,14 @@
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
<div class="select-kit-header-wrapper">
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
shouldDisplayClearableButton=shouldDisplayClearableButton
}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
shouldDisplayClearableButton=shouldDisplayClearableButton
}}
{{#if selectKit.options.showCaret}}
{{d-icon caretIcon class="caret-icon"}}
{{/if}}
{{#if selectKit.options.showCaret}}
{{d-icon caretIcon class="caret-icon"}}
{{/if}}
</div>

View File

@ -1,23 +0,0 @@
<div class="choices">
{{#each shownItems as |item|}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=item
selectKit=selectKit
}}
{{/each}}
{{#if hasHiddenItems}}
<div class="x-more-item" data-hidden-count={{hiddenItemsCount}}>
{{i18n "x_more" count=hiddenItemsCount}}
</div>
{{/if}}
{{#unless hasReachedMaximumSelection}}
<div class="choice input-wrapper">
{{component selectKit.options.filterComponent
selectKit=selectKit
id=(concat selectKit.uniqueID "-filter")
}}
</div>
{{/unless}}
</div>

View File

@ -1,19 +1,21 @@
{{#if icons}}
<div class="future-date-input-selector-icons">
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
</div>
{{/if}}
<div class="select-kit-header-wrapper">
{{#if icons}}
<div class="future-date-input-selector-icons">
{{#each icons as |icon|}} {{d-icon icon}} {{/each}}
</div>
{{/if}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
}}
{{#if selectedContent.datetime}}
<span class="future-date-input-selector-datetime">
{{selectedContent.datetime}}
</span>
{{/if}}
{{#if selectedContent.datetime}}
<span class="future-date-input-selector-datetime">
{{selectedContent.datetime}}
</span>
{{/if}}
{{d-icon caretIcon class="caret-icon"}}
{{d-icon caretIcon class="caret-icon"}}
</div>

View File

@ -1,7 +0,0 @@
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
}}
{{d-icon caretIcon class="caret-icon"}}

View File

@ -1,11 +1,16 @@
{{#each tags as |tag|}}
{{#d-button
translatedTitle=tag.value
icon="times"
action=(action "deselectTag")
actionParam=tag.value
class=tag.classNames
}}
{{discourse-tag tag.value noHref=true}}
{{/d-button}}
{{/each}}
{{#if tags}}
<div class="mini-tag-chooser-selected-collection selected-tags">
{{#each tags as |tag|}}
{{#d-button
translatedTitle=tag.value
icon="times"
action=(action selectKit.deselect)
actionParam=tag.value
class=tag.classNames
tabindex=0
}}
{{discourse-tag tag.value noHref=true}}
{{/d-button}}
{{/each}}
</div>
{{/if}}

View File

@ -8,6 +8,22 @@
}}
{{#select-kit/select-kit-body selectKit=selectKit id=(concat selectKit.uniqueID "-body")}}
{{component selectKit.options.filterComponent
selectKit=selectKit
id=(concat selectKit.uniqueID "-filter")
}}
{{#if selectedContent}}
<div class="selected-content">
{{#each selectedContent as |item|}}
{{component selectKit.options.selectedChoiceComponent
item=item
selectKit=selectKit
}}
{{/each}}
</div>
{{/if}}
{{#if selectKit.isLoading}}
<span class="is-loading">
{{#if site}}
@ -15,14 +31,6 @@
{{/if}}
</span>
{{else}}
{{#if selectKit.filter}}
{{#if selectKit.hasNoContent}}
<span class="no-content">
{{i18n "select_kit.no_content"}}
</span>
{{/if}}
{{/if}}
{{#each collections as |collection|}}
{{component (component-for-collection collection.identifier selectKit)
collection=collection
@ -30,8 +38,18 @@
value=value
}}
{{/each}}
{{#if selectKit.filter}}
{{#if selectKit.hasNoContent}}
<span class="no-content" role="alert">
{{i18n "select_kit.no_content"}}
</span>
{{else}}
<span class="results-count" role="alert">
{{i18n "select_kit.results_count" count=mainCollection.length}}
</span>
{{/if}}
{{/if}}
{{/if}}
{{/select-kit/select-kit-body}}
<div class="select-kit-wrapper"></div>
{{/unless}}

View File

@ -0,0 +1,3 @@
<span class="formated-selection">
{{formatedContent}}
</span>

View File

@ -1,18 +1,9 @@
<div class="choices">
{{#each selectedContent as |item|}}
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=item
selectKit=selectKit
}}
<div class="select-kit-header-wrapper">
{{#each icons as |icon|}}
{{d-icon icon}}
{{/each}}
{{#unless hasReachedMaximumSelection}}
<div class="choice input-wrapper">
{{component selectKit.options.filterComponent
selectKit=selectKit
id=(concat selectKit.uniqueID "-filter")
}}
</div>
{{/unless}}
{{multi-select/format-selected-content content=selectedContent selectKit=selectKit}}
{{d-icon caretIcon class="caret-icon"}}
</div>

View File

@ -1,7 +1,9 @@
<label class="filter-text">
{{i18n "user.user_notifications.filters.filter_by"}}
</label>
<label class="header-text">
{{i18n label}}
</label>
{{d-icon caretIcon class="caret-icon"}}
<div class="select-kit-header-wrapper">
<span class="filter-text">
{{i18n "user.user_notifications.filters.filter_by"}}
</span>
<span class="header-text">
{{i18n label}}
</span>
{{d-icon caretIcon class="caret-icon"}}
</div>

View File

@ -1,5 +1,7 @@
{{#if collection}}
{{#each collection.content as |item|}}
<li class="select-kit-error">{{item}}</li>
{{/each}}
{{#if collection.content}}
<ul class="select-kit-errors-collection">
{{#each collection.content as |item|}}
<li class="select-kit-error">{{item}}</li>
{{/each}}
</ul>
{{/if}}

View File

@ -1,7 +1,11 @@
{{#each collection.content as |item|}}
{{component (component-for-row collection.identifier item selectKit)
item=item
value=value
selectKit=selectKit
}}
{{/each}}
{{#if collection.content.length}}
<ul class="select-kit-collection" aria-live="polite" role="menu">
{{#each collection.content as |item|}}
{{component (component-for-row collection.identifier item selectKit)
item=item
value=value
selectKit=selectKit
}}
{{/each}}
</ul>
{{/if}}

View File

@ -1,14 +1,13 @@
{{#unless isHidden}}
{{!-- filter-input-search prevents 1password from attempting autocomplete --}}
{{input
tabindex=-1
tabindex=0
class="filter-input"
placeholder=placeholder
autocomplete="discourse"
autocorrect="off"
autocapitalize="off"
name="filter-input-search"
autofocus=selectKit.options.autofocus
spellcheck=false
value=(readonly selectKit.filter)
input=(action "onInput")

Some files were not shown because too many files have changed in this diff Show More