[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:
luvi
2022-06-30 09:29:42 +02:00
committed by Aaron Bohy
parent ec8ed943fc
commit bbc0f0c76e
4 changed files with 215 additions and 0 deletions
@@ -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"]);
});
});