[IMP] web: introduce useTooltip hook
This hook automatically generates custom tooltips (instead of native ones) for all elements with the "data-tooltip" attribute set, inside the component using it. The optional attribute "data-tooltip-position" can be used to enforce a specific tooltip position. This commit also makes the WebClient use this hook. Part-of: odoo/odoo#73311 Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
co-authored by
Lucas Perais
parent
6cfe05ae4b
commit
1fd54eeae7
@@ -0,0 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class Tooltip extends Component {}
|
||||
Tooltip.template = "web.Tooltip";
|
||||
Tooltip.props = {
|
||||
tooltip: String,
|
||||
Component: { optional: true },
|
||||
props: { optional: true },
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.o-tooltip {
|
||||
font-size: smaller;
|
||||
|
||||
.o-tooltip--string {
|
||||
background-color: $o-tooltip-title-background-color;
|
||||
font-weight: bold;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.o-tooltip--help {
|
||||
white-space: pre-line;
|
||||
padding: 8px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.o-tooltip--technical {
|
||||
padding: 8px;
|
||||
margin: 0 0 0 15px;
|
||||
list-style-type: circle;
|
||||
|
||||
.o-tooltip--technical--title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.Tooltip" owl="1">
|
||||
<div class="o-tooltip px-2 py-1">
|
||||
<t t-if="props.Component" t-component="props.Component" t-props="props.props" />
|
||||
<small t-else="" t-esc="props.tooltip"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,102 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useListener } from "@web/core/utils/hooks";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
* The useTooltip hook allows to display custom tooltips on every elements with
|
||||
* a "data-tooltip" attribute inside the Component using the hook. This attribute
|
||||
* can be set on elements for which we prefer a custom tooltip instead of the
|
||||
* native one displaying the value of the "title" attribute.
|
||||
*
|
||||
* Usage (with a parent somewhere using the useTooltip hook):
|
||||
* <button data-tooltip="This is a tooltip">Do something</button>
|
||||
*
|
||||
* The ideal position of the tooltip can be specified thanks to the attribute
|
||||
* "data-tooltip-position":
|
||||
* <button data-tooltip="This is a tooltip" data-tooltip-position="left">Do something</button>
|
||||
*/
|
||||
const OPEN_DELAY = 400;
|
||||
const CLOSE_DELAY = 200;
|
||||
export function useTooltip() {
|
||||
const popover = usePopover();
|
||||
let openTooltipTimeout;
|
||||
let closeTooltip;
|
||||
let target = null;
|
||||
|
||||
/**
|
||||
* Closes the currently opened tooltip if any, or prevent it from opening.
|
||||
*/
|
||||
function cleanup() {
|
||||
browser.clearTimeout(openTooltipTimeout);
|
||||
if (closeTooltip) {
|
||||
closeTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the tooltip after a delay, if the event's target has a data-tooltip
|
||||
* attribute.
|
||||
*
|
||||
* @param {MouseEvent} ev a "mouseenter" event
|
||||
*/
|
||||
function onMouseEnter(ev) {
|
||||
const dataset = ev.target.dataset;
|
||||
const tooltip = dataset.tooltip;
|
||||
|
||||
let Component, props;
|
||||
if ("tooltipComponent" in dataset) {
|
||||
({ Component, props } = JSON.parse(dataset["tooltipComponent"]));
|
||||
Component = registry.category("tooltips").get(Component, null);
|
||||
}
|
||||
if (tooltip || Component) {
|
||||
cleanup();
|
||||
target = ev.target;
|
||||
openTooltipTimeout = browser.setTimeout(() => {
|
||||
const position = dataset.tooltipPosition;
|
||||
closeTooltip = popover.add(
|
||||
target,
|
||||
Tooltip,
|
||||
{ tooltip, Component, props },
|
||||
{ position }
|
||||
);
|
||||
}, OPEN_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
// Track mouse position to be able to detect that we are no longer hovering
|
||||
// the target, thus that we should close the tooltip
|
||||
let positionX;
|
||||
let positionY;
|
||||
useListener("mousemove", (ev) => {
|
||||
positionX = ev.x;
|
||||
positionY = ev.y;
|
||||
});
|
||||
|
||||
// Regularly check that the target is still in the DOM and we're still
|
||||
// hovering it, because if not, we have to close the tooltip
|
||||
browser.setInterval(() => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (!document.body.contains(target)) {
|
||||
return cleanup(); // target is no longer in the DOM
|
||||
}
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
if (
|
||||
positionX < targetRect.left ||
|
||||
positionX > targetRect.right ||
|
||||
positionY < targetRect.top ||
|
||||
positionY > targetRect.bottom
|
||||
) {
|
||||
return cleanup(); // mouse is no longer hovering the target
|
||||
}
|
||||
}, CLOSE_DELAY);
|
||||
|
||||
// Listen (using event delegation) to "mouseenter" events on all nodes with
|
||||
// the "data-tooltip" attribute, to open the tooltip.
|
||||
useListener("mouseenter", "[data-tooltip]", onMouseEnter, { capture: true });
|
||||
}
|
||||
@@ -130,6 +130,84 @@ export function useEffect(effect, computeDependencies = () => [NaN]) {
|
||||
onWillUnmount(() => cleanup());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useListener
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The useListener hook offers an alternative to Owl's classical event
|
||||
* registration mechanism (with attribute 't-on-eventName' in xml).
|
||||
*
|
||||
* It is especially useful for abstract components, meant to be extended by
|
||||
* specific ones. If those abstract components need to define event handlers,
|
||||
* but don't have any template (because the template completely depends on
|
||||
* specific cases), then using the 't-on' mechanism isn't adequate, as the
|
||||
* handlers would be lost by the template override. In this case, using this
|
||||
* hook instead is more convenient.
|
||||
*
|
||||
* Usage: like all Owl hooks, this function has to be called in the
|
||||
* constructor of an Owl component:
|
||||
*
|
||||
* useListener('click', () => { console.log('clicked'); });
|
||||
*
|
||||
* It also allows to do event delegation, by specifying a native query selector
|
||||
* as second argument. In this case, the handler is only called if the event
|
||||
* is triggered on an element matching the given selector.
|
||||
*
|
||||
* useListener('click', 'button', () => { console.log('clicked'); });
|
||||
*
|
||||
* Note: components that alter the event's target (e.g. Portal) are not
|
||||
* expected to behave as expected with event delegation.
|
||||
*
|
||||
* @param {string} eventName the name of the event
|
||||
* @param {string} [querySelector] a JS native selector for event delegation
|
||||
* @param {function} handler the event handler (will be bound to the component)
|
||||
* @param {Object} [options] to be passed to addEventListener as options.
|
||||
* Useful for listening in the capture phase
|
||||
*/
|
||||
export function useListener(eventName, querySelector, handler, options = {}) {
|
||||
if (typeof arguments[1] !== "string") {
|
||||
querySelector = null;
|
||||
handler = arguments[1];
|
||||
options = arguments[2] || {};
|
||||
}
|
||||
if (typeof handler !== "function") {
|
||||
throw new Error("The handler must be a function");
|
||||
}
|
||||
|
||||
const comp = useComponent();
|
||||
let boundHandler;
|
||||
if (querySelector) {
|
||||
boundHandler = function (ev) {
|
||||
let el = ev.target;
|
||||
let target;
|
||||
while (el && !target) {
|
||||
if (el.matches(querySelector)) {
|
||||
target = el;
|
||||
} else if (el === comp.el) {
|
||||
el = null;
|
||||
} else {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
if (el) {
|
||||
handler.call(comp, ev);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
boundHandler = handler.bind(comp);
|
||||
}
|
||||
useEffect(
|
||||
() => {
|
||||
comp.el.addEventListener(eventName, boundHandler, options);
|
||||
return () => {
|
||||
comp.el.removeEventListener(eventName, boundHandler, options);
|
||||
};
|
||||
},
|
||||
() => []
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useService
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ActionContainer } from "./actions/action_container";
|
||||
import { NavBar } from "./navbar/navbar";
|
||||
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useTooltip } from "@web/core/tooltip/tooltip_hook";
|
||||
import { NotUpdatable } from "../core/utils/components";
|
||||
import { MainComponentsContainer } from "../core/main_components_container";
|
||||
import { useOwnDebugContext } from "../core/debug/debug_context";
|
||||
@@ -44,6 +45,7 @@ export class WebClient extends Component {
|
||||
() => []
|
||||
);
|
||||
useExternalListener(window, "click", this.onGlobalClick, { capture: true });
|
||||
useTooltip();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { PopoverContainer } from "@web/core/popover/popover_container";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { useTooltip } from "@web/core/tooltip/tooltip_hook";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils";
|
||||
|
||||
const { Component, mount, useState } = owl;
|
||||
const { xml } = owl.tags;
|
||||
|
||||
const mainComponents = registry.category("main_components");
|
||||
|
||||
/**
|
||||
* Creates and mounts a parent component that use the "useTooltip" hook.
|
||||
*
|
||||
* @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute
|
||||
* @param {Object} [options]
|
||||
* @param {Object} options.mockSetTimeout the mocked setTimeout to use (by default, calls the
|
||||
* callback directly)
|
||||
* @returns {Promise<Component>}
|
||||
*/
|
||||
async function makeParent(Child, options = {}) {
|
||||
const fixture = getFixture();
|
||||
|
||||
// add the popover service to the registry -> will add the PopoverContainer
|
||||
// to the mainComponentRegistry
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
registry.category("services").add("popover", popoverService);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponents.getEntries();
|
||||
useTooltip();
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Child/>
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
|
||||
<t t-component="Component[1].Component" t-props="Component[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>`;
|
||||
Parent.components = { PopoverContainer, Child };
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: options.mockSetTimeout || ((fn) => fn()),
|
||||
setInterval: options.mockSetInterval || ((fn) => fn()),
|
||||
});
|
||||
|
||||
const comp = await mount(Parent, { env, target: fixture });
|
||||
registerCleanup(() => comp.destroy());
|
||||
return comp;
|
||||
}
|
||||
|
||||
QUnit.module("Tooltip hook", () => {
|
||||
QUnit.test("basic rendering", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button data-tooltip="hello">Action</button>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
let simulateInterval;
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval });
|
||||
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
parent.el.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
parent.el.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
const buttonRect = parent.el.querySelector("button").getBoundingClientRect();
|
||||
const x = buttonRect.right + 10;
|
||||
const y = buttonRect.bottom + 10;
|
||||
await triggerEvent(parent.el, "button", "mousemove", {
|
||||
pageX: x,
|
||||
layerX: x,
|
||||
screenX: x,
|
||||
pageY: y,
|
||||
layerY: y,
|
||||
screenY: y,
|
||||
});
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("remove element with opened tooltip", async (assert) => {
|
||||
let compState;
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ visible: true });
|
||||
compState = this.state;
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<button t-if="state.visible" data-tooltip="hello">Action</button>
|
||||
</div>`;
|
||||
let simulateInterval;
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
const parent = await makeParent(MyComponent, { mockSetInterval });
|
||||
|
||||
assert.containsOnce(parent, "button");
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
parent.el.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
|
||||
compState.visible = false;
|
||||
await nextTick();
|
||||
assert.containsNone(parent, "button");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("rendering with several tooltips", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<button class="button_1" data-tooltip="tooltip 1">Action 1</button>
|
||||
<button class="button_2" data-tooltip="tooltip 2">Action 2</button>
|
||||
</div>`;
|
||||
const parent = await makeParent(MyComponent);
|
||||
|
||||
assert.containsNone(parent, ".o_popover_container .o_popover");
|
||||
parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 1");
|
||||
parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseleave"));
|
||||
parent.el.querySelector("button.button_2").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 2");
|
||||
});
|
||||
|
||||
QUnit.test("positionning", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<div style="height: 400px; padding: 40px">
|
||||
<button class="default" data-tooltip="default">Default</button>
|
||||
<button class="top" data-tooltip="top" data-tooltip-position="top">Top</button>
|
||||
<button class="right" data-tooltip="right" data-tooltip-position="right">Right</button>
|
||||
<button class="bottom" data-tooltip="bottom" data-tooltip-position="bottom">Bottom</button>
|
||||
<button class="left" data-tooltip="left" data-tooltip-position="left">Left</button>
|
||||
</div>`;
|
||||
const parent = await makeParent(MyComponent);
|
||||
|
||||
// default
|
||||
parent.el.querySelector("button.default").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "default");
|
||||
assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm");
|
||||
|
||||
// top
|
||||
parent.el.querySelector("button.top").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "top");
|
||||
assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--tm");
|
||||
|
||||
// right
|
||||
parent.el.querySelector("button.right").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "right");
|
||||
assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--rm");
|
||||
|
||||
// bottom
|
||||
parent.el.querySelector("button.bottom").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "bottom");
|
||||
assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm");
|
||||
|
||||
// left
|
||||
parent.el.querySelector("button.left").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(parent, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "left");
|
||||
assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--lm");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useBus, useEffect, useListener, useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, nextTick } from "../../helpers/utils";
|
||||
import { click, getFixture, nextTick } from "../../helpers/utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
|
||||
const { Component, mount, tags, useState } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
@@ -230,6 +231,93 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.module("useListener");
|
||||
|
||||
QUnit.test("useListener: simple usecase", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useListener("click", () => assert.step("click"));
|
||||
}
|
||||
}
|
||||
MyComponent.template = tags.xml`<button>Click Me</button>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, { env, target });
|
||||
registerCleanup(() => {
|
||||
comp.destroy();
|
||||
});
|
||||
|
||||
await click(comp.el);
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.test("useListener: event delegation", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.flag = true;
|
||||
useListener("click", "button", () => assert.step("click"));
|
||||
}
|
||||
}
|
||||
MyComponent.template = tags.xml`
|
||||
<div>
|
||||
<button t-if="flag">Click Here</button>
|
||||
<button t-else="">
|
||||
<span>or Here</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, { env, target });
|
||||
registerCleanup(() => {
|
||||
comp.destroy();
|
||||
});
|
||||
|
||||
await click(comp.el);
|
||||
assert.verifySteps([]);
|
||||
await click(comp.el.querySelector("button"));
|
||||
assert.verifySteps(["click"]);
|
||||
|
||||
comp.flag = false;
|
||||
await comp.render();
|
||||
await click(comp.el.querySelector("button span"));
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.test("useListener: event delegation with capture option", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.flag = false;
|
||||
useListener("click", "button", () => assert.step("click"), { capture: true });
|
||||
}
|
||||
}
|
||||
MyComponent.template = tags.xml`
|
||||
<div>
|
||||
<button t-if="flag">Click Here</button>
|
||||
<button t-else="">
|
||||
<span>or Here</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, { env, target });
|
||||
registerCleanup(() => {
|
||||
comp.destroy();
|
||||
});
|
||||
|
||||
await click(comp.el);
|
||||
assert.verifySteps([]);
|
||||
await click(comp.el.querySelector("button"));
|
||||
assert.verifySteps(["click"]);
|
||||
|
||||
comp.flag = false;
|
||||
await comp.render();
|
||||
await click(comp.el.querySelector("button span"));
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.module("useService");
|
||||
|
||||
QUnit.test("useService: unavailable service", async function (assert) {
|
||||
|
||||
@@ -149,7 +149,7 @@ TRANSLATED_ELEMENTS = {
|
||||
TRANSLATED_ATTRS = dict.fromkeys({
|
||||
'string', 'add-label', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title', 'aria-label',
|
||||
'aria-keyshortcuts', 'aria-placeholder', 'aria-roledescription', 'aria-valuetext',
|
||||
'value_label',
|
||||
'value_label', 'data-tooltip',
|
||||
}, lambda e: True)
|
||||
|
||||
def translate_attrib_value(node):
|
||||
|
||||
Reference in New Issue
Block a user