[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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user