From bbc0f0c76e21875a84d56ae532f6d53334e4973b Mon Sep 17 00:00:00 2001 From: luvi Date: Thu, 23 Jun 2022 07:40:21 +0000 Subject: [PATCH] [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 --- .../static/src/core/colorlist/colorlist.js | 54 +++++++++ .../static/src/core/colorlist/colorlist.scss | 39 +++++++ .../static/src/core/colorlist/colorlist.xml | 15 +++ .../web/static/tests/core/colorlist_tests.js | 107 ++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 addons/web/static/src/core/colorlist/colorlist.js create mode 100644 addons/web/static/src/core/colorlist/colorlist.scss create mode 100644 addons/web/static/src/core/colorlist/colorlist.xml create mode 100644 addons/web/static/tests/core/colorlist_tests.js diff --git a/addons/web/static/src/core/colorlist/colorlist.js b/addons/web/static/src/core/colorlist/colorlist.js new file mode 100644 index 00000000000..28ffecd3032 --- /dev/null +++ b/addons/web/static/src/core/colorlist/colorlist.js @@ -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 }, +}; diff --git a/addons/web/static/src/core/colorlist/colorlist.scss b/addons/web/static/src/core/colorlist/colorlist.scss new file mode 100644 index 00000000000..865c4e858fe --- /dev/null +++ b/addons/web/static/src/core/colorlist/colorlist.scss @@ -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; + } +} diff --git a/addons/web/static/src/core/colorlist/colorlist.xml b/addons/web/static/src/core/colorlist/colorlist.xml new file mode 100644 index 00000000000..33a7abcbe99 --- /dev/null +++ b/addons/web/static/src/core/colorlist/colorlist.xml @@ -0,0 +1,15 @@ + + + + +
+ +
+
+ +
diff --git a/addons/web/static/tests/core/colorlist_tests.js b/addons/web/static/tests/core/colorlist_tests.js new file mode 100644 index 00000000000..3d8d2ae85aa --- /dev/null +++ b/addons/web/static/tests/core/colorlist_tests.js @@ -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} + */ +async function mountComponent(Picker, props) { + serviceRegistry.add("ui", uiService); + target = getFixture(); + + class Parent extends Component {} + Parent.template = xml/* xml */ ` + +
+ `; + + 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"]); + }); +});