[FIX] web: various dropdown style issues

Various styling fixes.
Issues were brought by odoo/odoo@3b73095302

closes odoo/odoo#74752

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Bruno Boi
2021-08-10 12:58:02 +00:00
parent d46097eb73
commit 0bb93b272c
14 changed files with 182 additions and 179 deletions
@@ -4,6 +4,8 @@
<t t-name="web.DebugMenu" owl="1">
<Dropdown class="o_debug_manager"
beforeOpen="getElements"
position="'bottom-end'"
tag="'li'"
togglerClass="'btn btn-link'">
<t t-set-slot="toggler">
<span class="fa fa-bug"/>
@@ -15,7 +17,7 @@
<span t-else="" t-esc="element.description"/>
</DropdownItem>
</t>
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider"/>
<li t-if="element.type == 'separator'" role="separator" class="dropdown-divider"/>
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props"/>
</t>
</Dropdown>
@@ -67,13 +67,17 @@ export class Dropdown extends Component {
this.hasParentDropdown = this.env.inDropdown;
useSubEnv({ inDropdown: true });
this.rootTag = this.props.tag || (this.hasParentDropdown ? "li" : "div");
const position =
this.props.position || (this.hasParentDropdown ? "right-start" : "bottom-start");
// Set up key navigation -----------------------------------------------
useDropdownNavigation();
// Set up toggler and positioning --------------------------------------
const positioningOptions = {
popper: "menuRef",
position:
this.props.position || (this.hasParentDropdown ? "right-start" : "bottom-start"),
directionFlipOrder: { right: "rl", bottom: "bt", top: "tb", left: "lr" },
};
if (this.props.toggler === "parent") {
// Add parent click listener to handle toggling
useEffect(
@@ -94,11 +98,11 @@ export class Dropdown extends Component {
);
// Position menu relatively to parent element
usePosition(() => this.el.parentElement, { popper: "menuRef", position });
usePosition(() => this.el.parentElement, positioningOptions);
} else {
// Position menu relatively to inner toggler
const togglerRef = useRef("togglerRef");
usePosition(() => togglerRef.el, { popper: "menuRef", position });
usePosition(() => togglerRef.el, positioningOptions);
}
}
@@ -67,6 +67,7 @@
&_menu {
@extend .dropdown-menu;
position: relative;
&.o_dropdown_menu_right {
// FIXME: does commenting this break anything?
@@ -74,9 +75,8 @@
}
& > li.o_dropdown {
&.navigated > .o_dropdown_toggler {
// FIXME: without this there is no selection feedback
// @extend :hover;
&.focus > .o_dropdown_toggler {
background-color: $dropdown-link-hover-bg;
}
& > .o_dropdown_toggler {
// FIXME: without this there is no selection feedback
@@ -117,11 +117,8 @@
// FIXME: does commenting this break anything?
//@extend .disabled;
}
&.active > :first-child {
// FIXME: without this there is no selection feedback
// @extend :hover;
background-color: rgba(0, 0, 0, 0.08);
color: #091124;
&.focus {
background-color: $dropdown-link-hover-bg;
}
}
}
@@ -5,7 +5,7 @@
<t
t-tag="rootTag"
class="o_dropdown"
t-att-class="{ 'o-dropdown--open': state.open, 'o_dropdown_item': hasParentDropdown and rootTag === 'li' }"
t-att-class="{ show: state.open, o_dropdown_item: hasParentDropdown and rootTag === 'li' }"
t-on-dropdown-item-selected="onItemSelected"
>
<button
@@ -19,7 +19,7 @@ import { scrollTo } from "../utils/scrolling";
const { hooks } = owl;
const { useComponent, useRef } = hooks;
const ACTIVE_MENU_ELEMENT_CLASS = "active";
const ACTIVE_MENU_ELEMENT_CLASS = "focus";
const MENU_ELEMENTS_SELECTORS = [":scope > li.o_dropdown_item", ":scope > li.o_dropdown"];
const NEXT_ACTIVE_INDEX_FNS = {
FIRST: () => 0,
@@ -46,108 +46,105 @@ export function useDropdownNavigation() {
const menuRef = useRef("menuRef");
/** @type {MenuElement[]} */
let menuElements = [];
useEffect(
(open) => {
if (!open) {
return;
}
// Prepare MenuElements
const addedListeners = [];
/** @type {NodeListOf<HTMLElement>} */
const queryResult = menuRef.el.querySelectorAll(MENU_ELEMENTS_SELECTORS.join());
for (const el of queryResult) {
const isSubDropdown = el.classList.contains("o_dropdown");
const isSubDropdownOpen = () => el.classList.contains("o-dropdown--open");
const togglerClick = () => {
const toggler = el.querySelector(":scope > .o_dropdown_toggler");
if (toggler) {
toggler.dispatchEvent(new MouseEvent("click", { bubbles: false }));
}
};
let subDropdownTimeout;
const closeSubDropdown = () => {
browser.clearTimeout(subDropdownTimeout);
subDropdownTimeout = browser.setTimeout(() => {
if (isSubDropdownOpen()) {
togglerClick();
}
}, 200);
};
const openSubDropdown = (immediate = false) => {
browser.clearTimeout(subDropdownTimeout);
subDropdownTimeout = browser.setTimeout(
() => {
if (!isSubDropdownOpen()) {
togglerClick();
}
},
immediate ? 0 : 200
);
};
const makeOnlyActive = () => {
// Make all others inactive
for (const menuElement of menuElements) {
if (menuElement.el === el) {
continue;
}
menuElement.el.classList.remove(ACTIVE_MENU_ELEMENT_CLASS);
if (menuElement.isSubDropdown) {
menuElement.closeSubDropdown();
}
}
// Make myself active
el.classList.add(ACTIVE_MENU_ELEMENT_CLASS);
};
/** @type {MenuElement} */
const menuElement = {
el,
get isActive() {
return el.classList.contains(ACTIVE_MENU_ELEMENT_CLASS);
},
makeOnlyActive,
get isSubDropdownOpen() {
return isSubDropdownOpen();
},
isSubDropdown,
closeSubDropdown,
openSubDropdown,
};
menuElements.push(menuElement);
// Set up selection listeners
const elementListeners = {
click: makeOnlyActive,
mouseenter: () => {
if (!mouseSelectionActive) {
mouseSelectionActive = true;
} else {
menuElement.makeOnlyActive();
if (isSubDropdown) {
openSubDropdown();
}
}
},
};
for (const [eventType, listener] of Object.entries(elementListeners)) {
menuElement.el.addEventListener(eventType, listener);
}
addedListeners.push([menuElement, elementListeners]);
}
return () => {
menuElements = [];
mouseSelectionActive = true;
// Clear mouse selection listeners
for (const [menuElement, listeners] of addedListeners) {
for (const [eventType, listener] of Object.entries(listeners)) {
menuElement.el.removeEventListener(eventType, listener);
}
useEffect(() => {
if (!comp.state.open) {
return;
}
// Prepare MenuElements
const addedListeners = [];
/** @type {NodeListOf<HTMLElement>} */
const queryResult = menuRef.el.querySelectorAll(MENU_ELEMENTS_SELECTORS.join());
for (const el of queryResult) {
const isSubDropdown = el.classList.contains("o_dropdown");
const isSubDropdownOpen = () => el.classList.contains("show");
const togglerClick = () => {
const toggler = el.querySelector(":scope > .o_dropdown_toggler");
if (toggler) {
toggler.dispatchEvent(new MouseEvent("click", { bubbles: false }));
}
};
},
() => [comp.state.open]
);
let subDropdownTimeout;
const closeSubDropdown = () => {
browser.clearTimeout(subDropdownTimeout);
subDropdownTimeout = browser.setTimeout(() => {
if (isSubDropdownOpen()) {
togglerClick();
}
}, 200);
};
const openSubDropdown = (immediate = false) => {
browser.clearTimeout(subDropdownTimeout);
subDropdownTimeout = browser.setTimeout(
() => {
if (!isSubDropdownOpen()) {
togglerClick();
}
},
immediate ? 0 : 200
);
};
const makeOnlyActive = () => {
// Make all others inactive
for (const menuElement of menuElements) {
if (menuElement.el === el) {
continue;
}
menuElement.el.classList.remove(ACTIVE_MENU_ELEMENT_CLASS);
if (menuElement.isSubDropdown) {
menuElement.closeSubDropdown();
}
}
// Make myself active
el.classList.add(ACTIVE_MENU_ELEMENT_CLASS);
};
/** @type {MenuElement} */
const menuElement = {
el,
get isActive() {
return el.classList.contains(ACTIVE_MENU_ELEMENT_CLASS);
},
makeOnlyActive,
get isSubDropdownOpen() {
return isSubDropdownOpen();
},
isSubDropdown,
closeSubDropdown,
openSubDropdown,
};
menuElements.push(menuElement);
// Set up selection listeners
const elementListeners = {
click: makeOnlyActive,
mouseenter: () => {
if (!mouseSelectionActive) {
mouseSelectionActive = true;
} else {
menuElement.makeOnlyActive();
if (isSubDropdown) {
openSubDropdown();
}
}
},
};
for (const [eventType, listener] of Object.entries(elementListeners)) {
menuElement.el.addEventListener(eventType, listener);
}
addedListeners.push([menuElement, elementListeners]);
}
return () => {
menuElements = [];
mouseSelectionActive = true;
// Clear mouse selection listeners
for (const [menuElement, listeners] of addedListeners) {
for (const [eventType, listener] of Object.entries(listeners)) {
menuElement.el.removeEventListener(eventType, listener);
}
}
};
});
// Set up active menu element helpers --------------------------------------
/**
@@ -208,7 +205,11 @@ export function useDropdownNavigation() {
enter: () => {
const menuElement = getActiveMenuElement();
if (menuElement) {
menuElement.el.click();
if (menuElement.isSubDropdown) {
menuElement.openSubDropdown(true);
} else {
menuElement.el.click();
}
}
},
escape: comp.close,
@@ -9,6 +9,8 @@ import { debounce, throttle } from "../utils/timing";
* container?: HTMLElement;
* margin?: number;
* position?: Position;
* directionFlipOrder?: DirectionFlipOrder;
* variantFlipOrder?: VariantFlipOrder;
* }} Options
*
* @typedef {{
@@ -36,6 +38,14 @@ import { debounce, throttle } from "../utils/timing";
* }} VariantsData
*
* @typedef {"top" | "left" | "bottom" | "right"} Direction
* @typedef {"start" | "middle" | "end"} Variant
*
* @typedef {{[direction in Direction]: string}} DirectionFlipOrder
* values are successive DirectionsDataKey represented as a single string
*
* @typedef {{[variant in Variant]: string}} VariantFlipOrder
* values are successive VariantsDataKey represented as a single string
*
* @typedef {Direction
* | "top-start" | "top-middle" | "top-end"
* | "left-start" | "left-middle" | "left-end"
@@ -49,13 +59,13 @@ const { hooks } = owl;
const { useComponent, useExternalListener, useRef } = hooks;
const POPPER_CLASS = "o-popper-position";
const DIRECTION_FLIP_ORDER = { top: "tbrl", right: "rltb", bottom: "btrl", left: "lrbt" };
const VARIANT_FLIP_ORDER = { start: "sme", middle: "mse", end: "ems" };
/** @type {Options} */
export const DEFAULTS = {
margin: 0,
position: "bottom",
directionFlipOrder: { top: "tbrl", right: "rltb", bottom: "btrl", left: "lrbt" },
variantFlipOrder: { start: "sme", middle: "mse", end: "ems" },
};
/**
@@ -72,12 +82,12 @@ export const DEFAULTS = {
* - a method returning style to apply to the popper for a given direction and variant
*/
export function computePositioning(reference, popper, options) {
const { container, margin, position } = options;
const { container, margin, position, directionFlipOrder, variantFlipOrder } = options;
// Retrieve directions and variants
const [directionKey, variantKey = "middle"] = position.split("-");
const directions = DIRECTION_FLIP_ORDER[directionKey];
const variants = VARIANT_FLIP_ORDER[variantKey];
const directions = directionFlipOrder[directionKey];
const variants = variantFlipOrder[variantKey];
// Boxes
const popBox = popper.getBoundingClientRect();
@@ -89,8 +89,8 @@
}
}
.o-dropdown{
&--open > .o_dropdown_toggler {
.o-dropdown {
&.show > .o_dropdown_toggler {
@extend %-main-navbar-entry-active;
}
}
@@ -1,5 +0,0 @@
.o_switch_company_menu {
.o_dropdown_menu {
min-width: 100%;
}
}
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.SwitchCompanyMenu" owl="1">
<Dropdown class="o_switch_company_menu">
<Dropdown class="o_switch_company_menu" position="'bottom-end'" tag="'li'">
<t t-set-slot="toggler">
<span t-attf-class="{{env.isSmall ? 'fa fa-building-o' : 'oe_topbar_name'}}">
<t t-if="!env.isSmall"><t t-esc="currentCompany.name"/></t>
@@ -18,45 +18,43 @@
<t t-name="web.SwitchCompanyItem" owl="1">
<div>
<DropdownItem class="p-0 bg-white">
<t t-set="isCompanySelected" t-value="selectedCompanies.includes(company.id)"/>
<t t-set="isCurrent" t-value="company.id === companyService.currentCompany.id"/>
<DropdownItem class="p-0 bg-white">
<div class="d-flex" data-menu="company" t-att-data-company-id="company.id">
<div
role="menuitemcheckbox"
t-att-aria-checked="isCompanySelected"
t-att-aria-label="company.name"
t-att-title="(isCompanySelected ? 'Hide ' : 'Show ') + company.name + ' content.'"
tabindex="0"
class="border-right toggle_company o_py"
t-attf-class="{{isCurrent ? 'border-primary' : ''}}"
t-on-click.stop="toggleCompany(company.id)">
<div class="d-flex" data-menu="company" t-att-data-company-id="company.id">
<div
role="menuitemcheckbox"
t-att-aria-checked="isCompanySelected"
t-att-aria-label="company.name"
t-att-title="(isCompanySelected ? 'Hide ' : 'Show ') + company.name + ' content.'"
tabindex="0"
class="border-right toggle_company o_py"
t-attf-class="{{isCurrent ? 'border-primary' : ''}}"
t-on-click.stop="toggleCompany(company.id)">
<span class="btn btn-light border-0 p-2">
<i class="fa fa-fw py-2" t-att-class="isCompanySelected ? 'fa-check-square text-primary' : 'fa-square-o'"/>
</span>
</div>
<div
role="button"
t-att-aria-pressed="isCurrent"
t-att-aria-label="'Switch to ' + company.name "
t-att-title="'Switch to ' + company.name "
tabindex="0"
class="d-flex flex-grow-1 align-items-center py-0 log_into pl-2 o_py"
t-att-class="isCurrent ? 'alert-primary ml-1 mr-2' : 'btn btn-light font-weight-normal border-0'"
t-on-click="logIntoCompany(company.id)">
<span
class='company_label pr-3'
t-att-class="isCurrent ? 'text-900 font-weight-bold' : 'ml-1'">
<t t-esc="company.name"/>
</span>
</div>
<span class="btn btn-light border-0 p-2">
<i class="fa fa-fw py-2" t-att-class="isCompanySelected ? 'fa-check-square text-primary' : 'fa-square-o'"/>
</span>
</div>
</DropdownItem>
</div>
<div
role="button"
t-att-aria-pressed="isCurrent"
t-att-aria-label="'Switch to ' + company.name "
t-att-title="'Switch to ' + company.name "
tabindex="0"
class="d-flex flex-grow-1 align-items-center py-0 log_into pl-2 o_py"
t-att-class="isCurrent ? 'alert-primary ml-1 mr-2' : 'btn btn-light font-weight-normal border-0'"
t-on-click="logIntoCompany(company.id)">
<span
class='company_label pr-3'
t-att-class="isCurrent ? 'text-900 font-weight-bold' : 'ml-1'">
<t t-esc="company.name"/>
</span>
</div>
</div>
</DropdownItem>
</t>
</templates>
@@ -7,10 +7,4 @@
object-fit: cover;
transform: translateY(-2px);
}
.o_dropdown_menu {
// FIXME weird hack to keep menu from taking all viewport width
min-width: 100%;
position: sticky;
}
}
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.UserMenu" owl="1">
<Dropdown class="o_user_menu" t-on-dropdown-item-selected.stop="onDropdownItemSelected">
<Dropdown class="o_user_menu" tag="'li'" t-on-dropdown-item-selected.stop="onDropdownItemSelected">
<t t-set-slot="toggler">
<img class="rounded-circle o_user_avatar" t-att-src="source" alt="User"/> <span class="oe_topbar_name"><t t-esc="user.name"/><t t-if="env.debug" t-esc="' (' + user.db.name + ')'"/></span>
</t>
@@ -99,11 +99,11 @@ QUnit.module("DebugMenu", (hooks) => {
await click(debugManager.el.querySelector("button.o_dropdown_toggler"));
debugManagerEl = debugManager.el;
assert.containsN(debugManagerEl, "ul.o_dropdown_menu li.o_dropdown_item", 3);
assert.containsOnce(debugManagerEl, "div.dropdown-divider");
assert.containsOnce(debugManagerEl, "li.dropdown-divider");
const children = [...(debugManagerEl.querySelector("ul.o_dropdown_menu").children || [])];
assert.deepEqual(
children.map((el) => el.tagName),
["LI", "LI", "DIV", "LI"]
["LI", "LI", "LI", "LI"]
);
const items =
[...debugManagerEl.querySelectorAll("ul.o_dropdown_menu li.o_dropdown_item span")] ||
@@ -629,7 +629,7 @@ QUnit.module("Components", ({ beforeEach }) => {
// Navigate with arrows
assert.containsNone(
parent.el,
".o_dropdown_menu > .active",
".o_dropdown_menu > .focus",
"menu should not have any active items"
);
@@ -654,7 +654,7 @@ QUnit.module("Components", ({ beforeEach }) => {
for (const step of scenarioSteps) {
triggerHotkey(step.hotkey);
await nextTick();
assert.hasClass(parent.el.querySelector(".o_dropdown_menu > .active"), step.expected);
assert.hasClass(parent.el.querySelector(".o_dropdown_menu > .focus"), step.expected);
}
// Select last one activated in previous scenario (item1)
@@ -717,7 +717,7 @@ QUnit.module("Components", ({ beforeEach }) => {
});
QUnit.test("multi-level dropdown: keynav", async (assert) => {
assert.expect(119);
assert.expect(126);
class Parent extends owl.Component {
onItemSelected(ev) {
const { payload } = ev.detail;
@@ -808,7 +808,9 @@ QUnit.module("Components", ({ beforeEach }) => {
{ hotkey: "home", highlighted: ["second", "third", "third-first"] },
{ hotkey: "end", highlighted: ["second", "third", "third-last"] },
{ hotkey: "end", highlighted: ["second", "third", "third-last"] },
{ hotkey: "enter", selected: "third-last" },
{ hotkey: "arrowleft", highlighted: ["second", "third"] },
{ hotkey: "enter", highlighted: ["second", "third", "third-first"] },
{ hotkey: "enter", selected: "third-first" },
{ hotkey: "alt+1" },
{ hotkey: "alt+e", selected: false },
{ hotkey: "alt+f", selected: "first-last" },
@@ -826,7 +828,7 @@ QUnit.module("Components", ({ beforeEach }) => {
await nextTick();
if (step.highlighted !== undefined) {
let index = 0;
const activeElements = parent.el.querySelectorAll(".active");
const activeElements = parent.el.querySelectorAll(".focus");
assert.ok(
activeElements.length === step.highlighted.length,
`step ${stepIndex}: all active elements to check are found`
@@ -56,7 +56,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
const scMenu = await createSwitchCompanyMenu();
assert.strictEqual(scMenu.el.tagName.toUpperCase(), "DIV");
assert.strictEqual(scMenu.el.tagName.toUpperCase(), "LI");
assert.hasClass(scMenu.el, "o_switch_company_menu");
assert.strictEqual(scMenu.el.textContent, "Hermit");