Skip to Content
main_components data_display

Block UI

Odoo 19 main_components — Block 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
BlockUI
Category
main_components
Module
web
Slug
blockui
Nav group
data_display