[IMP] web: add bottomArea slot for SelectMenu

This commit allows the customization of the bottom area of the SelectMenu
component. The slot can show additional information, of handle any feature
related to the state of the component, such as adding elements, etc. It can
support any kind of content, so it could be used to display all kind of
useful content.

Two test have been added using this custom slot, asserting its presence and
testing an interaction with its content.

This changes is required to use the SelectMenu component in Knowledge, in
task-3134491

closes odoo/odoo#117890

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
luvi
2023-04-14 10:44:56 +02:00
parent 3cb3a226da
commit 585abd4847
4 changed files with 133 additions and 28 deletions
@@ -81,6 +81,7 @@ export class SelectMenu extends Component {
this.state = useState({
choices: [],
displayedOptions: [],
searchValue: "",
});
this.inputRef = useRef("inputRef");
this.inputContainerRef = useRef("inputContainerRef");
@@ -128,6 +129,8 @@ export class SelectMenu extends Component {
}
onOpened() {
this.state.searchValue = "";
// Using useAutofocus inside the dropdown does not
// work properly so we set the focus manually.
if (this.inputRef.el) {
@@ -157,6 +160,7 @@ export class SelectMenu extends Component {
onInput(searchString) {
this.filterOptions(searchString);
this.state.searchValue = searchString;
// Get reference to dropdown container and scroll to the top.
const inputContainer = this.inputContainerRef.el;
@@ -31,7 +31,7 @@
max-height: 350px;
border: 1px solid $o-brand-primary;
.o_select_menu_input {
.o_select_menu_input, .o_select_menu_bottom_area {
background-color: $dropdown-bg;
}
.o_select_menu_group {
@@ -5,7 +5,7 @@
<Dropdown
class="`o_select_menu border w-auto rounded-2 overflow-hidden ${props.class || ''}`"
togglerClass="`o_select_menu_toggler btn btn-secondary w-100 bg-light ${props.togglerClass || ''} ${canDeselect ? 'o_can_deselect' : ''}`"
menuClass="`o_select_menu_menu${props.searchable ? ' pt-0' : ''}${props.multiSelect ? ' o_select_menu_multi_select' : ''}`"
menuClass="`o_select_menu_menu${props.searchable ? ' py-0' : ''}${props.multiSelect ? ' o_select_menu_multi_select' : ''}`"
position="'bottom-fit'"
beforeOpen.bind="filterOptions"
onOpened.bind="onOpened"
@@ -61,6 +61,9 @@
</t>
</DropdownItem>
</t>
<div t-if="props.slots and props.slots.bottomArea" class="o_select_menu_bottom_area position-sticky bottom-0">
<t t-slot="bottomArea" data="state"/>
</div>
</Dropdown>
</t>
+124 -26
View File
@@ -12,6 +12,7 @@ import {
click,
triggerEvent,
nextTick,
editInput,
} from "../helpers/utils";
import { Component, useState, xml } from "@odoo/owl";
@@ -226,30 +227,33 @@ QUnit.module("Web Components", (hooks) => {
);
});
QUnit.test("Value with no corresponding choices displays as if no choice was selected", async (assert) => {
class Parent extends Component {
static components = { SelectMenu };
static template = xml`
QUnit.test(
"Value with no corresponding choices displays as if no choice was selected",
async (assert) => {
class Parent extends Component {
static components = { SelectMenu };
static template = xml`
<SelectMenu
choices="this.choices"
value="this.state.value"
/>
`;
setup() {
this.choices = [
{ label: "World", value: "world" },
{ label: "Hello", value: "hello" },
];
this.state = useState({ value: "coucou"});
setup() {
this.choices = [
{ label: "World", value: "world" },
{ label: "Hello", value: "hello" },
];
this.state = useState({ value: "coucou" });
}
setValue(newValue) {
this.state.value = newValue;
}
}
setValue(newValue) {
this.state.value = newValue;
}
}
await mount(Parent, target, { env });
assert.equal(getValue(), "", `The toggler should be empty`);
});
await mount(Parent, target, { env });
assert.equal(getValue(), "", `The toggler should be empty`);
}
);
QUnit.test("Changing value props properly updates the selected choice", async (assert) => {
class Parent extends Component {
@@ -265,7 +269,7 @@ QUnit.module("Web Components", (hooks) => {
{ label: "Z", value: { hello: "world" } },
{ label: "A", value: { paper: "company" } },
];
this.state = useState({ value: { paper: "company" }});
this.state = useState({ value: { paper: "company" } });
}
setValue(newValue) {
this.state.value = newValue;
@@ -273,11 +277,7 @@ QUnit.module("Web Components", (hooks) => {
}
const comp = await mount(Parent, target, { env });
assert.equal(
getValue(),
"A",
`The select value shoud be "A"`
);
assert.equal(getValue(), "A", `The select value shoud be "A"`);
comp.setValue({ hello: "world" });
await nextTick();
@@ -325,7 +325,7 @@ QUnit.module("Web Components", (hooks) => {
);
QUnit.test(
"When the \"required\" props is set to true, the clear button is not shown",
'When the "required" props is set to true, the clear button is not shown',
async (assert) => {
class Parent extends Component {
setup() {
@@ -349,12 +349,20 @@ QUnit.module("Web Components", (hooks) => {
`;
const parent = await mount(Parent, target, { env });
assert.containsNone(target, ".o_select_menu_toggler_clear", 'When the value is not set, there is no "clear" button');
assert.containsNone(
target,
".o_select_menu_toggler_clear",
'When the value is not set, there is no "clear" button'
);
parent.setValue("hello");
await nextTick();
assert.strictEqual(getValue(), "Hello");
assert.containsNone(target, ".o_select_menu_toggler_clear", 'When the value is set, there is no "clear" button');
assert.containsNone(
target,
".o_select_menu_toggler_clear",
'When the value is set, there is no "clear" button'
);
}
);
@@ -442,6 +450,96 @@ QUnit.module("Web Components", (hooks) => {
assert.strictEqual(target.querySelector(".coolClass").textContent, "Hello");
});
QUnit.test(
"Custom template for the bottom area of the dropdown using a slot",
async (assert) => {
class Parent extends Component {
setup() {
this.choices = [
{ label: "Hello", value: "hello" },
{ label: "World", value: "world" },
{ label: "How", value: "how" },
{ label: "Are", value: "are" },
{ label: "You", value: "you" },
];
this.state = useState({ value: ["world", "you"] });
}
onSelect(newValue) {
this.state.value = newValue;
}
}
Parent.components = { SelectMenu };
Parent.template = xml`
<SelectMenu
choices="choices"
multiSelect="true"
onSelect.bind="onSelect"
value="state.value"
>
<span class="select_menu_test">Select something</span>
<t t-set-slot="bottomArea" t-slot-scope="select">
<span>
<t t-esc="state.value.length"/> items selected
</span>
</t>
</SelectMenu>
`;
await mount(Parent, target, { env });
await open();
assert.strictEqual(
target.querySelector(".o_select_menu_bottom_area").textContent.trim(),
"2 items selected"
);
await click(target, ".o_select_menu_item:nth-child(2)");
await open();
assert.strictEqual(
target.querySelector(".o_select_menu_bottom_area").textContent.trim(),
"3 items selected"
);
}
);
QUnit.test("Custom slot for the bottom area sends the current search value", async (assert) => {
class Parent extends Component {
setup() {
this.choices = [
{ label: "Hello", value: "hello" },
{ label: "World", value: "world" },
];
}
onClick(value) {
assert.step(value + " clicked");
}
}
Parent.components = { SelectMenu };
Parent.template = xml`
<SelectMenu
choices="choices"
>
<span class="select_menu_test">Select something</span>
<t t-set-slot="bottomArea" t-slot-scope="select">
<div t-if="select.data.searchValue" class="px-2">
<button class="coolClass btn text-primary" t-on-click="() => this.onClick(select.data.searchValue)">
Do something with "<i t-esc="select.data.searchValue" />"
</button>
</div>
</t>
</SelectMenu>
`;
await mount(Parent, target, { env });
await open();
assert.containsNone(target, ".coolClass");
await editInput(target, ".o_select_menu_input input", "coucou");
assert.containsOnce(target, ".coolClass");
await click(target.querySelector(".coolClass"));
assert.verifySteps(["coucou clicked"]);
});
QUnit.test("Groups properly added in the select", async (assert) => {
class Parent extends Component {
setup() {