mirror of
https://github.com/discourse/discourse.git
synced 2026-08-10 04:58:31 -05:00
UX: overhaul of GroupSelector with Floatkit (#34685)
This PR modernizes the GroupSelector component by migrating from the legacy jQuery autocomplete to FloatKit’s DMultiSelect component. ### Changes * Migrated GroupSelector from an Ember Classic component to Glimmer component * Replaced jQuery-based autocomplete with FloatKit’s DMultiSelect component which handles search * New scroll-into-view modifier for smooth keyboard navigation (can be used later to replace d-autocomplete's similar functionality) * Improved FloatKit's `size` middleware option handling to allow passing through both minWidth and width props * Selected items are dynamically removed from dropdown options (no duplicates)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
@@ -17,6 +16,7 @@ import element from "discourse/helpers/element";
|
||||
import discourseDebounce from "discourse/lib/debounce";
|
||||
import { INPUT_DELAY } from "discourse/lib/environment";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import scrollIntoView from "discourse/modifiers/scroll-into-view";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
@@ -69,6 +69,17 @@ export default class DMultiSelect extends Component {
|
||||
return new TrackedAsyncData(value);
|
||||
}
|
||||
|
||||
get availableOptions() {
|
||||
if (!this.data.isResolved || !this.data.value) {
|
||||
return this.data.value;
|
||||
}
|
||||
|
||||
return this.data.value.filter(
|
||||
(item) =>
|
||||
!this.args.selection?.some((selected) => this.compare(item, selected))
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
search(event) {
|
||||
this.preselectedItem = null;
|
||||
@@ -77,7 +88,9 @@ export default class DMultiSelect extends Component {
|
||||
|
||||
@action
|
||||
focus(input) {
|
||||
input.focus();
|
||||
// Reset preselection on dropdown open to prevent unwanted scrolling
|
||||
this.preselectedItem = null;
|
||||
input.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -88,8 +101,15 @@ export default class DMultiSelect extends Component {
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.preselectedItem) {
|
||||
// Only toggle if we have a preselected item and it's in the available options
|
||||
if (
|
||||
this.preselectedItem &&
|
||||
this.availableOptions?.some((item) =>
|
||||
this.compare(item, this.preselectedItem)
|
||||
)
|
||||
) {
|
||||
this.toggle(this.preselectedItem, event);
|
||||
}
|
||||
}
|
||||
@@ -97,19 +117,19 @@ export default class DMultiSelect extends Component {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.data.value?.length) {
|
||||
if (!this.availableOptions?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.preselectedItem === null) {
|
||||
this.preselectedItem = this.data.value[0];
|
||||
this.preselectedItem = this.availableOptions[0];
|
||||
} else {
|
||||
const currentIndex = this.data.value.findIndex((item) =>
|
||||
const currentIndex = this.availableOptions.findIndex((item) =>
|
||||
this.compare(item, this.preselectedItem)
|
||||
);
|
||||
|
||||
if (currentIndex < this.data.value.length - 1) {
|
||||
this.preselectedItem = this.data.value[currentIndex + 1];
|
||||
if (currentIndex < this.availableOptions.length - 1) {
|
||||
this.preselectedItem = this.availableOptions[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,19 +137,19 @@ export default class DMultiSelect extends Component {
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.data.value?.length) {
|
||||
if (!this.availableOptions?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.preselectedItem === null) {
|
||||
this.preselectedItem = this.data.value[0];
|
||||
this.preselectedItem = this.availableOptions[0];
|
||||
} else {
|
||||
const currentIndex = this.data.value.findIndex((item) =>
|
||||
const currentIndex = this.availableOptions.findIndex((item) =>
|
||||
this.compare(item, this.preselectedItem)
|
||||
);
|
||||
|
||||
if (currentIndex > 0) {
|
||||
this.preselectedItem = this.data.value[currentIndex - 1];
|
||||
this.preselectedItem = this.availableOptions[currentIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,26 +159,29 @@ export default class DMultiSelect extends Component {
|
||||
remove(selectedItem, event) {
|
||||
event?.stopPropagation();
|
||||
|
||||
// Reset preselected item since the available options will change
|
||||
this.preselectedItem = null;
|
||||
|
||||
this.args.onChange?.(
|
||||
this.args.selection?.filter((item) => !this.compare(item, selectedItem))
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
isSelected(result) {
|
||||
return this.args.selection?.filter((item) => this.compare(item, result))
|
||||
.length;
|
||||
}
|
||||
|
||||
@action
|
||||
toggle(result, event) {
|
||||
event?.stopPropagation();
|
||||
|
||||
if (this.isSelected(result)) {
|
||||
this.remove(result, event);
|
||||
} else {
|
||||
this.args.onChange?.(makeArray(this.args.selection).concat(result));
|
||||
const currentSelection = makeArray(this.args.selection);
|
||||
|
||||
// Check if item is already selected
|
||||
if (currentSelection.some((item) => this.compare(item, result))) {
|
||||
return; // Don't add duplicates
|
||||
}
|
||||
|
||||
// Reset preselected item since the available options will change
|
||||
this.preselectedItem = null;
|
||||
|
||||
this.args.onChange?.(currentSelection.concat(result));
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -170,6 +193,10 @@ export default class DMultiSelect extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayText(item) {
|
||||
return item?.name;
|
||||
}
|
||||
|
||||
#resolveAsyncData(asyncData, context, resolve, reject) {
|
||||
return asyncData(context).then(resolve).catch(reject);
|
||||
}
|
||||
@@ -179,6 +206,12 @@ export default class DMultiSelect extends Component {
|
||||
@identifier="d-multi-select"
|
||||
@triggerComponent={{element "div"}}
|
||||
@triggerClass={{concatClass (if this.hasSelection "--has-selection")}}
|
||||
@visibilityOptimizer={{@visibilityOptimizer}}
|
||||
@placement={{@placement}}
|
||||
@allowedPlacements={{@allowedPlacements}}
|
||||
@offset={{@offset}}
|
||||
@matchTriggerMinWidth={{@matchTriggerMinWidth}}
|
||||
@matchTriggerWidth={{@matchTriggerWidth}}
|
||||
...attributes
|
||||
>
|
||||
<:trigger>
|
||||
@@ -188,6 +221,7 @@ export default class DMultiSelect extends Component {
|
||||
<button
|
||||
class="d-multi-select-trigger__selected-item"
|
||||
{{on "click" (fn this.remove item)}}
|
||||
title={{this.getDisplayText item}}
|
||||
>
|
||||
<span class="d-multi-select-trigger__selection-label">{{yield
|
||||
item
|
||||
@@ -241,24 +275,20 @@ export default class DMultiSelect extends Component {
|
||||
{{yield this.data.error to="error"}}
|
||||
</div>
|
||||
{{else if this.data.isResolved}}
|
||||
{{#if this.data.value}}
|
||||
{{#if this.availableOptions.length}}
|
||||
<div class="d-multi-select__search-results">
|
||||
{{#each this.data.value as |result|}}
|
||||
{{#each this.availableOptions as |result|}}
|
||||
<menu.item
|
||||
class={{concatClass
|
||||
"d-multi-select__result"
|
||||
(if (eq result this.preselectedItem) "--preselected" "")
|
||||
}}
|
||||
role="button"
|
||||
title={{this.getDisplayText result}}
|
||||
{{scrollIntoView (eq result this.preselectedItem)}}
|
||||
{{on "mouseenter" (fn (mut this.preselectedItem) result)}}
|
||||
{{on "click" (fn this.toggle result)}}
|
||||
>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.isSelected result}}
|
||||
class="d-multi-select__result-checkbox"
|
||||
/>
|
||||
|
||||
<span class="d-multi-select__result-label">
|
||||
{{yield result to="result"}}
|
||||
</span>
|
||||
|
||||
@@ -1,76 +1,99 @@
|
||||
/* eslint-disable ember/no-classic-components */
|
||||
import Component from "@ember/component";
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { array } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes, on } from "@ember-decorators/object";
|
||||
import $ from "jquery";
|
||||
import groupAutocomplete from "discourse/lib/autocomplete/group";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import DMultiSelect from "discourse/components/d-multi-select";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class GroupSelector extends Component {
|
||||
@discourseComputed("placeholderKey")
|
||||
placeholder(placeholderKey) {
|
||||
return placeholderKey ? i18n(placeholderKey) : "";
|
||||
@service siteSettings;
|
||||
|
||||
@tracked selectedGroups = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.initializeSelectedGroups();
|
||||
}
|
||||
|
||||
@observes("groupNames")
|
||||
_update() {
|
||||
if (this.canReceiveUpdates === "true") {
|
||||
this._initializeAutocomplete({ updateData: true });
|
||||
initializeSelectedGroups() {
|
||||
const groupNames = this.args.groupNames;
|
||||
if (isEmpty(groupNames)) {
|
||||
this.selectedGroups = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert groupNames (string or array) to array of group objects
|
||||
let names = Array.isArray(groupNames) ? groupNames : [groupNames];
|
||||
if (typeof groupNames === "string" && groupNames.includes(",")) {
|
||||
names = groupNames.split(",").map((name) => name.trim());
|
||||
}
|
||||
|
||||
// Create minimal group objects from names
|
||||
this.selectedGroups = names
|
||||
.filter((name) => name && name.length > 0)
|
||||
.map((name) => ({ id: name, name }));
|
||||
}
|
||||
|
||||
get placeholder() {
|
||||
return this.args.placeholderKey ? i18n(this.args.placeholderKey) : "";
|
||||
}
|
||||
|
||||
get loadFn() {
|
||||
return async (term) => {
|
||||
if (!this.args.groupFinder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.args.groupFinder(term);
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
handleSelectionChange(selectedGroups) {
|
||||
// Handle single selection mode
|
||||
if (this.args.single && selectedGroups.length > 1) {
|
||||
selectedGroups = [selectedGroups[selectedGroups.length - 1]];
|
||||
}
|
||||
|
||||
this.selectedGroups = selectedGroups;
|
||||
|
||||
const groupNames = selectedGroups.map((group) => group.name);
|
||||
|
||||
if (this.args.onChange) {
|
||||
this.args.onChange(groupNames.join(","));
|
||||
} else if (this.args.onChangeCallback) {
|
||||
this.args.onChangeCallback(groupNames.join(","), groupNames);
|
||||
}
|
||||
}
|
||||
|
||||
@on("didInsertElement")
|
||||
_initializeAutocomplete(opts) {
|
||||
let selectedGroups;
|
||||
let groupNames = this.groupNames;
|
||||
|
||||
$(this.element.querySelector("input")).autocomplete({
|
||||
debounced: true,
|
||||
allowAny: false,
|
||||
items: Array.isArray(groupNames)
|
||||
? groupNames
|
||||
: isEmpty(groupNames)
|
||||
? []
|
||||
: [groupNames],
|
||||
single: this.single,
|
||||
fullWidthWrap: this.fullWidthWrap,
|
||||
updateData: opts && opts.updateData ? opts.updateData : false,
|
||||
onChangeItems: (items) => {
|
||||
selectedGroups = items;
|
||||
|
||||
if (this.onChange) {
|
||||
this.onChange(items.join(","));
|
||||
} else if (this.onChangeCallback) {
|
||||
this.onChangeCallback(this.groupNames, selectedGroups);
|
||||
} else {
|
||||
this.set("groupNames", items.join(","));
|
||||
}
|
||||
},
|
||||
transformComplete: (g) => {
|
||||
return g.name;
|
||||
},
|
||||
dataSource: (term) => {
|
||||
return this.groupFinder(term).then((groups) => {
|
||||
if (!selectedGroups) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups.filter((group) => {
|
||||
return !selectedGroups.any((s) => s === group.name);
|
||||
});
|
||||
});
|
||||
},
|
||||
template: groupAutocomplete,
|
||||
});
|
||||
@action
|
||||
compareGroups(a, b) {
|
||||
return a.name === b.name;
|
||||
}
|
||||
|
||||
<template>
|
||||
<input
|
||||
placeholder={{this.placeholder}}
|
||||
class="group-selector"
|
||||
type="text"
|
||||
name="groups"
|
||||
/>
|
||||
<div class="group-selector-wrapper">
|
||||
<DMultiSelect
|
||||
@selection={{this.selectedGroups}}
|
||||
@loadFn={{this.loadFn}}
|
||||
@onChange={{this.handleSelectionChange}}
|
||||
@label={{this.placeholder}}
|
||||
@compareFn={{this.compareGroups}}
|
||||
@placement="bottom-start"
|
||||
@allowedPlacements={{array "top-start" "bottom-start"}}
|
||||
@matchTriggerWidth={{true}}
|
||||
@matchTriggerMinWidth={{true}}
|
||||
class="group-selector"
|
||||
>
|
||||
<:selection as |group|>
|
||||
{{group.name}}
|
||||
</:selection>
|
||||
<:result as |group|>
|
||||
{{group.name}}
|
||||
</:result>
|
||||
</DMultiSelect>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import Modifier from "ember-modifier";
|
||||
|
||||
/**
|
||||
* Modifier to scroll an element into view when a condition is met
|
||||
* Usage: {{scroll-into-view shouldScroll options}}
|
||||
*
|
||||
* @param {boolean} shouldScroll - Whether to scroll this element into view
|
||||
* @param {Object} options - ScrollIntoView options (behavior, block, inline)
|
||||
*/
|
||||
export default class ScrollIntoViewModifier extends Modifier {
|
||||
modify(element, [shouldScroll, options = {}]) {
|
||||
if (!shouldScroll || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollOptions = {
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
...options,
|
||||
};
|
||||
|
||||
element.scrollIntoView(scrollOptions);
|
||||
}
|
||||
}
|
||||
+106
-5
@@ -77,7 +77,7 @@ module("Integration | Component | d-multi-select", function (hooks) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
await click(".d-multi-select__result:nth-child(2)");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
@@ -138,7 +138,7 @@ module("Integration | Component | d-multi-select", function (hooks) {
|
||||
|
||||
await click(".d-multi-select-trigger");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
await click(".d-multi-select__result:nth-child(2)");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
@@ -174,7 +174,7 @@ module("Integration | Component | d-multi-select", function (hooks) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
await click(".d-multi-select__result:nth-child(2)");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
@@ -184,19 +184,68 @@ module("Integration | Component | d-multi-select", function (hooks) {
|
||||
.hasText("bar");
|
||||
});
|
||||
|
||||
test("unselect item", async function (assert) {
|
||||
test("selected items are removed from dropdown", async function (assert) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
|
||||
// Initially both options should be visible
|
||||
assert.dom(".d-multi-select__result").exists({ count: 2 });
|
||||
assert.dom(".d-multi-select__result:nth-child(1)").hasText("foo");
|
||||
assert.dom(".d-multi-select__result:nth-child(2)").hasText("bar");
|
||||
|
||||
// Select the first item
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
// Check that item appears in selection and only one option remains
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("foo");
|
||||
assert.dom(".d-multi-select__result").exists({ count: 1 });
|
||||
assert.dom(".d-multi-select__result:nth-child(1)").hasText("bar");
|
||||
|
||||
// Select the second item
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
// Check that both items are selected and no options remain
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("foo");
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
||||
.hasText("bar");
|
||||
assert.dom(".d-multi-select__result").doesNotExist();
|
||||
assert.dom(".d-multi-select__search-no-results").exists();
|
||||
});
|
||||
|
||||
test("unselect item via pill removal", async function (assert) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
await click(".d-multi-select__result:nth-child(2)");
|
||||
await click(".d-multi-select__result:nth-child(1)");
|
||||
|
||||
// Both items should be selected now and no options should remain
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("foo");
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
||||
.hasText("bar");
|
||||
assert.dom(".d-multi-select__result").doesNotExist();
|
||||
|
||||
// Remove the first selected item via pill
|
||||
await click(".d-multi-select-trigger__selected-item:nth-child(1)");
|
||||
|
||||
// Now only bar should be selected and foo should reappear in dropdown
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("bar");
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(2)")
|
||||
.doesNotExist();
|
||||
|
||||
// The dropdown should still be open and show the unselected item
|
||||
assert.dom(".d-multi-select__result").exists({ count: 1 });
|
||||
assert.dom(".d-multi-select__result:nth-child(1)").hasText("foo");
|
||||
});
|
||||
|
||||
test("preselect item", async function (assert) {
|
||||
@@ -219,4 +268,56 @@ module("Integration | Component | d-multi-select", function (hooks) {
|
||||
|
||||
assert.dom(".d-multi-select__error").hasText("Error: error");
|
||||
});
|
||||
|
||||
test("prevents duplicate selections when pressing Enter multiple times", async function (assert) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
|
||||
// Navigate to first item and press Enter
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
|
||||
// Verify first item is selected
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item")
|
||||
.exists({ count: 1 }, "Should have exactly one selected item");
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("foo");
|
||||
|
||||
// Press Enter again multiple times on the same item
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
|
||||
// Verify still only one item is selected (no duplicates)
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item")
|
||||
.exists(
|
||||
{ count: 1 },
|
||||
"Should still have exactly one selected item after multiple Enter presses"
|
||||
);
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item:nth-child(1)")
|
||||
.hasText("foo");
|
||||
});
|
||||
|
||||
test("Enter key does nothing when no item is preselected", async function (assert) {
|
||||
await render(<template><TestComponent /></template>);
|
||||
await click(".d-multi-select-trigger");
|
||||
|
||||
// Press Enter without navigating to any item first
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Enter");
|
||||
|
||||
// Verify no items are selected
|
||||
assert
|
||||
.dom(".d-multi-select-trigger__selected-item")
|
||||
.doesNotExist("Should not select any items when no item is preselected");
|
||||
|
||||
// Verify both options are still available
|
||||
assert.dom(".d-multi-select__result").exists({ count: 2 });
|
||||
assert.dom(".d-multi-select__result:nth-child(1)").hasText("foo");
|
||||
assert.dom(".d-multi-select__result:nth-child(2)").hasText("bar");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,22 +194,21 @@ function buildShiftMiddleware(options, detectOverflowOptions) {
|
||||
}
|
||||
|
||||
function buildMatchSizeMiddleware(options) {
|
||||
if (options.matchTriggerWidth) {
|
||||
if (options.matchTriggerWidth || options.matchTriggerMinWidth) {
|
||||
return size({
|
||||
apply({ rects, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
const styleProps = {};
|
||||
const widthValue = `${rects.reference.width}px`;
|
||||
|
||||
if (options.matchTriggerMinWidth) {
|
||||
return size({
|
||||
apply({ rects, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
});
|
||||
if (options.matchTriggerWidth) {
|
||||
styleProps.width = widthValue;
|
||||
}
|
||||
|
||||
if (options.matchTriggerMinWidth) {
|
||||
styleProps.minWidth = widthValue;
|
||||
}
|
||||
|
||||
Object.assign(elements.floating.style, styleProps);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
@import "footer-nav";
|
||||
@import "form-template-field";
|
||||
@import "group-member-dropdown";
|
||||
@import "group-selector";
|
||||
@import "groups-form-membership-fields";
|
||||
@import "hashtag";
|
||||
@import "horizontal-overflow-nav";
|
||||
|
||||
@@ -11,15 +11,8 @@
|
||||
border: 1px solid var(--primary-medium);
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
&:not(.--has-selection) {
|
||||
background-color: var(--primary-medium);
|
||||
color: var(--secondary);
|
||||
|
||||
.btn-transparent .d-icon-angle-down {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
&.-expanded {
|
||||
@include default-focus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +26,7 @@
|
||||
.d-multi-select-trigger__label {
|
||||
font-size: var(--font-0);
|
||||
line-height: normal;
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
.d-multi-select__search-no-result,
|
||||
@@ -65,7 +59,6 @@
|
||||
padding: 0.25em;
|
||||
gap: 0.25em;
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.d-multi-select__search-no-results {
|
||||
@@ -108,10 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.d-multi-select__result-checkbox {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.d-multi-select__skeletons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -150,3 +139,18 @@
|
||||
animation: dmultiselect-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
border-radius: var(--d-border-radius);
|
||||
}
|
||||
|
||||
.fk-d-menu[data-identifier="d-multi-select"] {
|
||||
z-index: z("modal", "dialog") + 1;
|
||||
|
||||
// This ensures child elements of the main d-multi-select container maintain the same width
|
||||
// even if content width changes as options are selected to ensure consistency with the UI
|
||||
// and click-on-outside behaviour
|
||||
.fk-d-menu__inner-content {
|
||||
width: inherit;
|
||||
|
||||
.d-multi-select__content {
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.group-selector-wrapper {
|
||||
.d-multi-select-trigger__selected-item > {
|
||||
.d-multi-select-trigger__selection-label {
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.d-multi-select-trigger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
-1
@@ -587,7 +587,6 @@ export default class PostEventBuilder extends Component {
|
||||
@label="discourse_post_event.builder_modal.invitees.label"
|
||||
>
|
||||
<GroupSelector
|
||||
@fullWidthWrap={{true}}
|
||||
@groupFinder={{this.groupFinder}}
|
||||
@groupNames={{@model.event.rawInvitees}}
|
||||
@onChangeCallback={{this.setRawInvitees}}
|
||||
|
||||
@@ -262,8 +262,9 @@ describe "Post event", type: :system do
|
||||
find(".toolbar-menu__options-trigger").click
|
||||
find("button[title='#{I18n.t("js.discourse_post_event.builder_modal.attach")}']").click
|
||||
find(".d-modal input[name=status][value=private]").click
|
||||
find(".d-modal input.group-selector").send_keys(group.name)
|
||||
find(".autocomplete.ac-group").click
|
||||
find(".group-selector").click
|
||||
find(".d-multi-select__search-input").send_keys(group.name)
|
||||
find(".d-multi-select__result", text: group.name).click
|
||||
find(".d-modal .custom-field-input").fill_in(with: "custom value")
|
||||
dropdown = PageObjects::Components::SelectKit.new(".available-recurrences")
|
||||
dropdown.expand
|
||||
@@ -277,7 +278,7 @@ describe "Post event", type: :system do
|
||||
post_event_page.edit
|
||||
|
||||
expect(find(".d-modal input[name=status][value=private]").checked?).to eq(true)
|
||||
expect(find(".d-modal")).to have_text(group.name)
|
||||
expect(find(".group-selector .d-multi-select-trigger__selection")).to have_text(group.name)
|
||||
expect(find(".d-modal .custom-field-input").value).to eq("custom value")
|
||||
expect(page).to have_selector(".d-modal .recurrence-until .date-picker") do |input|
|
||||
input.value == "#{1.year.from_now.year}-12-30"
|
||||
|
||||
Reference in New Issue
Block a user