[IMP] web: add ColorList component
This commit introduces a ColorList component which allows to choose a color from a list. There are several usecases for such a component in Odoo (e.g. ColorPicker, Many2ManyTags, KanbanRecord). Part-of: odoo/odoo#92475
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
|
||||
const { Component, useRef, useState, useExternalListener } = owl;
|
||||
|
||||
export class ColorList extends Component {
|
||||
setup() {
|
||||
this.colorlistRef = useRef("colorlist");
|
||||
this.state = useState({ isExpanded: false });
|
||||
useExternalListener(window, "click", this.onOutsideClick);
|
||||
}
|
||||
get colors() {
|
||||
return this.constructor.COLORS;
|
||||
}
|
||||
onColorSelected(id) {
|
||||
this.props.onColorSelected(id);
|
||||
this.state.isExpanded = false;
|
||||
}
|
||||
onOutsideClick(ev) {
|
||||
if (this.colorlistRef.el.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
this.state.isExpanded = false;
|
||||
}
|
||||
onToggle(focus = true) {
|
||||
this.state.isExpanded = !this.state.isExpanded;
|
||||
if (focus) {
|
||||
this.colorlistRef.el.firstElementChild.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColorList.COLORS = [
|
||||
_lt("No color"),
|
||||
_lt("Red"),
|
||||
_lt("Orange"),
|
||||
_lt("Yellow"),
|
||||
_lt("Light blue"),
|
||||
_lt("Dark purple"),
|
||||
_lt("Salmon pink"),
|
||||
_lt("Medium blue"),
|
||||
_lt("Dark blue"),
|
||||
_lt("Fuchsia"),
|
||||
_lt("Green"),
|
||||
_lt("Purple"),
|
||||
];
|
||||
ColorList.template = "web.ColorList";
|
||||
ColorList.props = {
|
||||
colors: Array,
|
||||
isExpanded: { type: Boolean, optional: true },
|
||||
onColorSelected: Function,
|
||||
togglerColor: { type: Number, optional: true },
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
.o_colorlist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
gap: 10px;
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
border: 1px solid white;
|
||||
box-shadow: 0 0 0 1px #c9ccd2;
|
||||
width: 22px;
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
// Set all the colors but the "no-color" one
|
||||
@for $size from 2 through length($o-colors) {
|
||||
.o_colorlist_item_color_#{$size - 1} {
|
||||
background-color: nth($o-colors, $size);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the "no-color", a red bar on white background
|
||||
.o_colorlist_item_color_0 {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
@include o-position-absolute(-2px, $left: 10px);
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 19px;
|
||||
transform: rotate(45deg);
|
||||
background-color: red;
|
||||
}
|
||||
&::after {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.ColorList" owl="1">
|
||||
<div class="o_colorlist" aria-atomic="true" t-ref="colorlist">
|
||||
<t t-if="props.togglerColor !== undefined and !state.isExpanded">
|
||||
<button t-on-click.prevent.stop="onToggle" role="menuitem" t-att-title="colors[props.togglerColor]" t-att-data-color="props.togglerColor" t-att-aria-label="colors[props.togglerColor]" t-attf-class="{{'btn o_colorlist_toggler o_colorlist_item_color_' + props.togglerColor }}"/>
|
||||
</t>
|
||||
<t t-else="" t-foreach="props.colors" t-as="colorId" t-key="colorId">
|
||||
<button t-on-click.prevent.stop="() => this.onColorSelected(colorId)" role="menuitem" t-att-title="colors[colorId]" t-att-data-color="colorId" t-att-aria-label="colors[colorId]" t-attf-class="{{'btn o_colorlist_item_color_' + colorId }}"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,107 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ColorList } from "@web/core/colorlist/colorlist";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, mount } from "../helpers/utils";
|
||||
|
||||
const { Component, xml } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
/**
|
||||
* @param {typeof ColorList} Picker
|
||||
* @param {Object} props
|
||||
* @returns {Promise<ColorList>}
|
||||
*/
|
||||
async function mountComponent(Picker, props) {
|
||||
serviceRegistry.add("ui", uiService);
|
||||
target = getFixture();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml/* xml */ `
|
||||
<t t-component="props.Picker" t-props="props.props"/>
|
||||
<div class="outsideDiv"/>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
if (!props.onColorSelected) {
|
||||
props.onColorSelected = () => {};
|
||||
}
|
||||
const parent = await mount(Parent, target, { env, props: { Picker, props } });
|
||||
return parent;
|
||||
}
|
||||
|
||||
QUnit.module("Components", () => {
|
||||
QUnit.module("ColorList");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await mountComponent(ColorList, {
|
||||
colors: [0, 9],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_colorlist");
|
||||
assert.containsN(target, "button", 2, "two buttons are available");
|
||||
const secondBtn = target.querySelectorAll(".o_colorlist button")[1];
|
||||
assert.strictEqual(
|
||||
secondBtn.attributes.title.value,
|
||||
"Fuchsia",
|
||||
"second button color is Fuchsia"
|
||||
);
|
||||
assert.hasClass(
|
||||
secondBtn,
|
||||
"o_colorlist_item_color_9",
|
||||
"second button has the corresponding class"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("toggler is available if togglerColor props is given", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const togglerColorId = 0;
|
||||
await mountComponent(ColorList, {
|
||||
colors: [4, 5, 6],
|
||||
togglerColor: togglerColorId,
|
||||
onColorSelected: (colorId) => assert.step("color #" + colorId + " is selected"),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_colorlist");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"only the toggler button is available"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_colorlist button"),
|
||||
"o_colorlist_item_color_" + togglerColorId,
|
||||
"toggler has the right class"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_colorlist button"));
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"toggler button is no longer visible"
|
||||
);
|
||||
assert.containsN(target, "button", 3, "three buttons are available");
|
||||
|
||||
await click(target.querySelector(".outsideDiv"));
|
||||
assert.containsN(target, "button", 1, "only one button is available");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"colorlist has been closed and toggler is visible"
|
||||
);
|
||||
|
||||
// reopen the colorlist and select a color
|
||||
await click(target.querySelector(".o_colorlist_toggler"));
|
||||
await click(target.querySelectorAll(".o_colorlist button")[2]);
|
||||
|
||||
assert.verifySteps(["color #6 is selected"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user