services
data_display
ui
Odoo 19 services — ui (core)
Live preview
Interactive
Source excerpt
web/static/src/core/ui/ui_service.js
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { throttleForAnimation } from "@web/core/utils/timing";
import { BlockUI } from "./block_ui";
import { browser } from "@web/core/browser/browser";
import { getTabableElements, isFocusable } from "@web/core/utils/ui";
import { getActiveHotkey } from "../hotkeys/hotkey_service";
import { EventBus, reactive, useEffect, useRef } from "@odoo/owl";
export const SIZES = { XS: 0, SM: 1, MD: 2, LG: 3, XL: 4, XXL: 5 };
export function getFirstAndLastTabableElements(el) {
const tabableEls = getTabableElements(el);
return [tabableEls[0], tabableEls[tabableEls.length - 1]];
}
/**
* This hook will set the UI active element
* when the caller component will mount/patch and
* only if the t-reffed element has some tabable elements
* or is itself focusable.
*
* The caller component could pass a `t-ref` value of its template
* to delegate the UI active element to another element than itself.
*
* @param {string} refName
*/
export function useActiveElement(refName) {
if (!refName) {
throw new Error("refName not given to useActiveElement");
}
const uiService = useService("ui");
const ref = useRef(refName);
function trapFocus(e) {
const hotkey = getActiveHotkey(e);
if (!["tab", "shift+tab"].includes(hotkey)) {
return;
}
const el = e.currentTarget;
const [firstTabableEl, lastTabableEl] = getFirstAndLastTabableElements(el);
if (!firstTabableEl && !lastTabableEl) {
e.preventDefault();
e.stopPropagation();
return;
}
switch (hotkey) {
case "tab":
if (document.activeElement === lastTabableEl) {
firstTabableEl.focus();
e.preventDefault();
e.stopPropagation();
}
break;
case "shift+tab":
if (document.activeElement === firstTabableEl) {
lastTabableEl.focus();
e.preventDefault();
e.stopPropagation();
}
break;
}
}
useEffect(
(el) => {
if (el) {
const [firstTabableEl] = getFirstAndLastTabableElements(el);
if (!firstTabableEl && !isFocusable(el)) {
// no tabable elements: no need to trap focus nor become the UI active element
return;
}
const oldActiveElement = document.activeElement;
uiService.activateElement(el);
el.addEventListener("keydown", trapFocus);
if (firstTabableEl) {
if (!el.contains(document.activeElement)) {
firstTabableEl.focus();
}
} else if (el !== document.activeElement) {
el.focus();
}
return async () => {
// Components are destroyed from top to bottom, meaning that this cleanup is
// called before the ones of children. As a consequence, event handlers added on
// the current active element in children aren't removed yet, and can thus be
// executed if we deactivate that active element right away (e.g. the blur and
// change events could be triggered). For that reason, we wait for a micro-tick.
await Promise.resolve();
uiService.deactivateElement(el);
el.removeEventListener("keydown", trapFocus);
/**
* In some cases, the current active element is not
* anymore in el (e.g. with ConfirmationDialog, the
* confirm button is disabled when clicked, so the
* focus is lost). In that case, we also want to restore
* the focus to the previous active element so we
* check if the current active element is the body
*/
if (
el.contains(document.activeElement) ||
document.activeElement === document.body
) {
oldActiveElement.focus();
}
};
}
},
() => [ref.el]
);
}
// window size handling
export const MEDIAS_BREAKPOINTS = [
{ maxWidth: 575 },
{ minWidth: 576, maxWidth: 767 },
Registry / API
- Registry name
ui- Category
services- Module
web- Slug
ui- Nav group
data_display