[REF] web, mail: badges design

Part-of: odoo/odoo#116641
This commit is contained in:
stefanorigano (SRI)
2023-05-12 22:59:13 +02:00
committed by Pierre Paridans
parent 051ac37232
commit ef31306130
24 changed files with 78 additions and 221 deletions
@@ -91,11 +91,11 @@ QUnit.module(
);
assert.containsN(
target,
'.o_field_widget[name="partner_ids"] .badge',
'.o_field_widget[name="partner_ids"] .o_tag',
2,
"there should be 2 tags"
);
const badges = target.querySelectorAll('.o_field_widget[name="partner_ids"] .badge');
const badges = target.querySelectorAll('.o_field_widget[name="partner_ids"] .o_tag');
assert.strictEqual(
badges[0].textContent.trim(),
"Jesus",
@@ -11,14 +11,11 @@
color: inherit;
margin-right: 0px;
}
.o_m2m_avatar, .o_m2m_avatar_empty {
.o_m2m_avatar_empty {
width: 20px;
height: 20px;
margin-left: 0px;
}
.o_m2m_avatar_empty {
background-color: $o-gray-300;
vertical-align: bottom;
}
@@ -21,7 +21,6 @@
<t t-name="mail.KanbanMany2ManyTagsAvatarUserField" t-inherit="web.KanbanMany2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
<TagsList position="attributes">
<attribute name="displayBadge">!props.readonly</attribute>
<attribute name="displayText">displayText</attribute>
</TagsList>
</t>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="FieldMany2ManyTagsEmailTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
<xpath expr="//span[contains(@t-attf-class, 'badge')]" position="replace">
<xpath expr="//span[contains(@class, 'o_tag')]" position="replace">
<div t-if="tag.email" t-attf-class="badge rounded-pill dropdown o_tag o_tag_color_0 #{tag.email.indexOf('@') &lt; 0 ? 'o_tag_error' : ''}" t-att-data-color="tag.colorIndex" t-att-data-index="tag_index" t-att-data-id="tag.id" t-att-title="tag.text">
<span class="o_badge_text" t-att-title="tag.email"><t t-esc="tag.text"/></span>
<a t-if="!readonly &amp;&amp; tag.onDelete" t-on-click.stop.prevent="tag.onDelete" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"/>
@@ -40,14 +40,14 @@ ColorList.COLORS = [
_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("Cyan"),
_lt("Purple"),
_lt("Almond"),
_lt("Teal"),
_lt("Blue"),
_lt("Raspberry"),
_lt("Green"),
_lt("Violet"),
];
ColorList.template = "web.ColorList";
ColorList.defaultProps = {
@@ -1,6 +1,6 @@
.o_colorlist {
button {
border: 1px solid $white;
border: 1px solid $dropdown-bg;
box-shadow: 0 0 0 1px $gray-500;
width: 22px;
height: 17px;
@@ -13,23 +13,12 @@
// 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);
$-bg: adjust-color(nth($o-colors, $size), $lightness: 25%, $saturation: 15%);
@include o-print-color($-bg, background-color, bg-opacity);
}
}
// 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;
}
background: linear-gradient(45deg, rgba($dropdown-bg, 0) 0%, rgba($dropdown-bg, 0) 48%, $danger 48%, $danger 52%, rgba($dropdown-bg, 0) 52%, rgba($dropdown-bg, 0) 100%);
}
@@ -4,10 +4,10 @@
<t t-name="web.ColorList" owl="1">
<div class="o_colorlist d-flex flex-wrap align-items-center mw-100 gap-2" aria-atomic="true" t-ref="colorlist">
<t t-if="!props.forceExpanded and !state.isExpanded">
<button t-on-click="onToggle" role="menuitem" t-att-title="colors[props.selectedColor]" t-att-data-color="props.selectedColor" t-att-aria-label="colors[props.selectedColor]" t-attf-class="btn p-0 o_colorlist_toggler o_colorlist_item_color_{{ props.selectedColor }}"/>
<button t-on-click="onToggle" role="menuitem" t-att-title="colors[props.selectedColor]" t-att-data-color="props.selectedColor" t-att-aria-label="colors[props.selectedColor]" t-attf-class="btn p-0 rounded-0 o_colorlist_toggler o_colorlist_item_color_{{ props.selectedColor }}"/>
</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 p-0 o_colorlist_item_color_{{ colorId }} {{ colorId === props.selectedColor ? 'o_colorlist_selected' : '' }}"/>
<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 p-0 rounded-0 o_colorlist_item_color_{{ colorId }} {{ colorId === props.selectedColor ? 'o_colorlist_selected' : '' }}"/>
</t>
</div>
</t>
@@ -9,6 +9,7 @@ export class TagsList extends Component {
displayText: true,
};
static props = {
className: { type: String, optional: true },
displayBadge: { type: Boolean, optional: true },
displayText: { type: Boolean, optional: true },
itemsVisible: { type: Number, optional: true },
@@ -1,63 +1,24 @@
.o_tag {
user-select: none;
font-size: var(--Tag-font-size, #{$font-size-sm});
max-width: var(--Tag-max-width, 100%);
.o_badge_text, a {
color: inherit;
line-height: 1.1;
}
.o_tag_badge_text {
font-size: 12px;
}
img {
height: 1.6em;
aspect-ratio: 1/1;
+ .o_tag_badge_text {
padding: 0.25em 0;
}
a {
color: inherit;
}
@for $size from 1 through length($o-colors) {
&.o_tag_color_#{$size - 1} {
@if $size == 1 {
& {
background-color: $o-view-background-color;
color: nth($o-colors, $size);
box-shadow: inset 0 0 0 1px;
}
&:focus-within {
color: darken(nth($o-colors, $size), 40%) ;
}
&::after {
background-color: nth($o-colors, $size);
}
} @else {
&, &::after {
background-color: nth($o-colors, $size);
color: color-contrast(nth($o-colors, $size));
}
$-bg: adjust-color(nth($o-colors, $size), $lightness: 25%, $saturation: 15%);
$-color: adjust-color(nth($o-colors, $size), $lightness: -40%, $saturation: -15%);
&, &::after {
@include o-print-color($-bg, background-color, bg-opacity);
@include o-print-color($-color, color, text-opacity);
}
}
}
}
.o_kanban_view .o_kanban_record .o_tag {
display: inline-block;
margin-right: 4px;
font-size: 11px;
font-weight: 500;
background-color: transparent;
color: inherit;
box-shadow: none;
@include o-kanban-tag-color;
span {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 100%;
}
}
}
@@ -3,15 +3,23 @@
<t t-name="web.TagsList" owl="1">
<t t-foreach="visibleTags" t-as="tag" t-key="tag.id or tag_index">
<span t-attf-class="{{`o_tag_color_${ tag.colorIndex or 0 } ${props.displayBadge ? 'badge rounded-pill' : ''}`}} o_tag d-inline-flex align-items-center" t-att-class="tag.className" tabindex="-1" t-att-data-color="tag.colorIndex" t-att-title="tag.text" t-on-click="(ev) => tag.onClick and tag.onClick(ev)" t-on-keydown="tag.onKeydown">
<img t-if="tag.img" t-att-src="tag.img" class="o_m2m_avatar rounded-circle" t-att-class="tag.imageClass"/>
<i t-if="tag.icon" t-attf-class="p-1 fa {{ tag.icon }}"/>
<div t-if="props.displayBadge and props.displayText" class="o_tag_badge_text px-1" t-esc="tag.text" />
<t t-elif="props.displayText">
<span />
<div t-esc="tag.text" />
</t>
<a tabIndex="-1" t-if="tag.onDelete" t-on-click.stop.prevent="(ev) => tag.onDelete and tag.onDelete(ev)" href="#" class="fa fa-times o_delete pe-1" title="Delete" aria-label="Delete"></a>
<span
class="o_tag position-relative d-inline-flex align-items-center user-select-none"
t-att-class="{
'o_avatar opacity-trigger-hover' : tag.img,
'o_badge badge rounded-pill': !tag.img,
}"
t-attf-class="{{ !tag.img ? 'o_tag_color_' + (tag.colorIndex ? tag.colorIndex : '0') : '' }}"
tabindex="-1"
t-att-data-color="tag.colorIndex"
t-att-title="tag.text"
t-on-click="(ev) => tag.onClick and tag.onClick(ev)"
t-on-keydown="tag.onKeydown">
<img t-if="tag.img" t-att-src="tag.img" class="o_avatar o_m2m_avatar rounded" t-att-class="tag.imageClass"/>
<i t-if="tag.icon" t-attf-class="p-1 fa {{ tag.icon }}" t-att-class="tag.className"/>
<div t-if="props.displayText" class="o_tag_badge_text" t-att-class="{'ms-1' : tag.img}" t-esc="tag.text" />
<a tabIndex="-1" t-if="tag.onDelete" t-on-click.stop.prevent="(ev) => tag.onDelete and tag.onDelete(ev)" href="#" class="o_delete oi oi-close ps-1" title="Delete" aria-label="Delete"/>
</span>
</t>
<span t-if="props.tags and otherTags.length" class="o_m2m_avatar_empty rounded-circle text-center fw-bold" data-tooltip-template="web.TagsList.Tooltip" data-tooltip-position="right" t-att-data-tooltip-info="tooltipInfo">
@@ -153,22 +153,6 @@
padding-bottom: 0;
}
// Many2OneAvatar
&.o_field_many2one_avatar {
.o_m2o_avatar > img, .o_m2o_avatar > .o_m2o_avatar_empty {
border-radius: 50%;
width: 19px;
height: 19px;
object-fit: cover;
margin-right: 4px;
}
.o_m2o_avatar_empty {
display: block;
background: $o-black;
opacity: .1;
}
}
// Many2many tags
&.o_field_many2manytags {
flex-flow: row wrap;
+2 -14
View File
@@ -336,20 +336,8 @@
}
// No Color
&:first-child > a {
position: relative;
&::before {
content: "";
@include o-position-absolute(-2px, $left: 10px);
display: block;
width: 1px;
height: 20px;
transform: rotate(45deg);
background-color: red;
}
&::after {
background-color: white;
}
&:first-child > a:after {
background: linear-gradient(45deg, rgba($dropdown-bg, 0) 0%, rgba($dropdown-bg, 0) 48%, $danger 48%, $danger 52%, rgba($dropdown-bg, 0) 52%, rgba($dropdown-bg, 0) 100%);
}
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ $form-range-thumb-bg: $primary !default;
// Badge
$badge-color: inherit !default;
$badge-padding-y: 0.25em !default;
$badge-padding-x: 0.4em !default;
$badge-padding-x: 0.82em !default;
// Placeholder color
$input-placeholder-color: $gray-300 !default;
@@ -200,7 +200,7 @@ $o-list-group-active-bg: lighten(saturate(adjust-hue($o-info, 15), 1.8), 50) !de
// == Badges
// Define a minimum width. This value is arbitrary and strictly font-related.
$o-badge-min-width: 2.5ch !default;
$o-badge-min-width: 3ch !default;
$o-nb-calendar-colors: 24 !default;
@@ -5,8 +5,8 @@ $o-list-group-header-color: lighten($o-brand-lightsecondary, 10%) !default;
// UI custom colors for tags, kanban records' colors, ...)
// Note: the first value is the old BS3 gray-light value
$o-colors: lighten(#000, 46.7%), #F06050, #F4A460, #F7CD1F, #6CC1ED, #814968,
#EB7E7F, #2C8397, #475577, #D6145F, #30C381, #9365B8 !default;
$o-colors: #a2a2a2, #ee2d2d, #dc8534, #e8bb1d, #5794dd, #9f628f, #db8865,
#41a9a2, #304be0, #ee2f8a, #61c36e, #9872e6 !default;
$o-colors-secondary: #aa4b6b, #30C381, #97743a, #F7CD1F, #4285F4, #8E24AA,
#D6145F, #173e43, #348F50, #AA3A38, #795548, #5e0231,
@@ -2,6 +2,8 @@
<templates xml:space="preserve">
<t t-name="web.KanbanMany2ManyTagsField" owl="1">
<TagsList tags="tags" displayBadge="false" />
<div class="d-flex flex-wrap gap-1">
<TagsList className="'o_kanban_tags'" tags="tags" displayBadge="false"/>
</div>
</t>
</templates>
@@ -158,7 +158,7 @@ export class Many2ManyTagsField extends Component {
focusTag(index) {
const autoCompleteParent = this.autoCompleteRef.el.parentElement;
const tags = autoCompleteParent.getElementsByClassName("badge");
const tags = autoCompleteParent.getElementsByClassName("o_tag");
if (tags.length) {
if (index === undefined) {
tags[tags.length - 1].focus();
@@ -219,8 +219,8 @@ export class Many2ManyTagsField extends Component {
}
const hotkey = getActiveHotkey(ev);
const autoCompleteParent = this.autoCompleteRef.el.parentElement;
const tags = [...autoCompleteParent.getElementsByClassName("badge")];
const closestTag = ev.target.closest(".badge");
const tags = [...autoCompleteParent.getElementsByClassName("o_tag")];
const closestTag = ev.target.closest(".o_tag");
const tagIndex = tags.indexOf(closestTag);
const input = this.autoCompleteRef.el.querySelector(".o-autocomplete--input");
switch (hotkey) {
@@ -11,11 +11,6 @@
.o_tags_input {
padding: 1px 0;
.o_tag {
padding-left: 0.6em;
padding-right: 0.6em;
}
}
.o_field_many2many_selection {
@@ -29,12 +24,7 @@
.badge {
flex: 0 0 auto;
margin: 1px 2px 1px 0;
border: none;
font-size: 12px;
user-select: none;
display: flex;
max-width: 100%;
&.dropdown {
cursor: pointer;
@@ -47,15 +37,6 @@
.o_badge_text, .o_tag_badge_text {
@include o-text-overflow(inline-block);
max-width: 200px;
color: inherit;
line-height: 1.1;
}
.o_delete {
color: inherit;
cursor: pointer;
padding-left: 4px;
line-height: 1;
}
}
}
@@ -3,7 +3,7 @@
<t t-name="web.Many2ManyTagsField" owl="1">
<div
class="o_field_tags d-inline-flex flex-wrap"
class="o_field_tags d-inline-flex flex-wrap gap-1"
t-att-class="{'o_tags_input o_input': !props.readonly}"
>
<TagsList tags="tags"/>
@@ -1,74 +1,22 @@
.o_field_widget.o_field_many2many_tags_avatar {
flex-flow: row wrap;
$-text-spacing-v: $o-line-height-base * 1em;
--img-text-gap-v: calc(var(--Avatar-size, #{$o-avatar-size}) - #{$-text-spacing-v});
.o_tags_input {
padding: 1px 0;
}
flex-flow: row wrap;
margin-top: calc(var(--img-text-gap-v) * -0.5); // using transform conflicts with dropdown position
.o_field_many2many_selection {
flex: 1 0 50px;
.o_input {
.o-autocomplete, .o_input {
height: 100%;
}
.o_input {
border: none;
}
}
.o_dropdown_button {
top: $o-input-padding-y;
}
.badge {
flex: 0 0 auto;
margin: 1px 2px 1px 0;
border: none;
font-size: 12px;
user-select: none;
display: flex;
max-width: 100%;
align-items: center;
padding: 0;
padding-right: 0.6rem;
box-shadow: inset 0 0 0 1px;
&.dropdown {
cursor: pointer;
}
a {
color: inherit;
}
.o_badge_text {
@include o-text-overflow(inline-block);
max-width: 200px;
color: inherit;
line-height: 1.1;
}
.o_delete {
color: inherit;
cursor: pointer;
padding-left: 4px;
line-height: 1;
}
img {
height: 1.4em;
width: 1.4em;
object-fit: cover;
}
.o_badge_text, .o_delete {
padding-top: 0.25em;
padding-bottom: 0.25em;
}
.o_tag_badge_text {
padding-left:2px;
}
}
}
.o_kanban_record {
@@ -85,10 +33,7 @@
}
.o_list_view .o_field_widget.o_field_many2many_tags_avatar {
.o_tags_input {
border: 0;
padding: 0;
}
--img-text-gap-v: 0;
.o_field_many2many_selection {
flex-basis: 40px;
@@ -5,7 +5,8 @@
<div t-attf-class="o_field_property_tag o_field_widget o_field_many2many_tags d-flex align-items-center {{props.readonly ? 'readonly' : ''}}">
<TagsList
tags="tagListItems"
displayBadge="displayBadge"/>
displayBadge="displayBadge"
className="props.canChangeTags ? '' : 'o_field_property_tag_readonly'"/>
<div
t-if="!props.readonly"
class="o_field_property_dropdown_menu o_input_dropdown mt-2">
@@ -55,6 +55,7 @@ kbd {
//== Badges
.badge {
min-width: $o-badge-min-width;
line-height: 1.1;
}
// Set position of our custom o-dropdown-menu (not bootstrap)
@@ -48,8 +48,8 @@ QUnit.module("Components", () => {
const secondBtn = target.querySelectorAll(".o_colorlist button")[1];
assert.strictEqual(
secondBtn.attributes.title.value,
"Fuchsia",
"second button color is Fuchsia"
"Raspberry",
"second button color is Raspberry"
);
assert.hasClass(
secondBtn,
@@ -455,24 +455,24 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
2,
"should have 2 records"
);
await click(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .o_tag .o_delete")
);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
});