[IMP] web: update owl from v2.0.7 to v2.0.9

This update contains the following commits:

[IMP] implement .alike suffix on props
[IMP] release: add version number on App
[IMP] app: add name as a config option
[FIX] runtime, compiler: fix refs getting set or unset incorrectly
[FIX] compiler: call translate function with correct string
[FIX] compiler: properly handle readonly attribute/readOnly property
[REF] blockdom,compiler: implement properties
[REF] tests: move properties tests in own file
[FIX] compiler: dynamic value on inputs doesn't turn 0 into empty string
[FIX] components: do not crash when binding anonymous function

More details at: https://github.com/odoo/owl/releases/tag/v2.0.9

Note that this owl update required a few adaptations in Odoo code. The
main problem was that some code would access references after the
component was unmounted. However, Owl is now stricter and properly
remove the reference.

closes odoo/odoo#115991

X-original-commit: a2952026f23858a8d34dcdab4ec8b467f7fc9bcf
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Géry Debongnie
2023-03-22 11:06:46 +01:00
parent 35a0cfa09f
commit 70a412f325
9 changed files with 234 additions and 202 deletions
@@ -43,9 +43,9 @@
value="dist_tag.analytic_account_name"
sources="sourcesAnalyticAccount"
autoSelect="true"
onSelect.bind="(option, params) => this.onSelect(option, params, dist_tag)"
onFocus.bind="() => this.groupAutocompleteFocus(plan.id)"
onInput.bind="(inputVal) => this.autoCompleteInput(dist_tag, inputVal)"/>
onSelect.alike="(option, params) => this.onSelect(option, params, dist_tag)"
onFocus.alike="() => this.groupAutocompleteFocus(plan.id)"
onInput.alike="(inputVal) => this.autoCompleteInput(dist_tag, inputVal)"/>
</td>
<td class="o_analytic_percentage">
<input
@@ -20883,6 +20883,9 @@
owl.onMounted(() => {
resizeObserver.observe(this.gridOverlayEl);
});
owl.onWillUnmount(() => {
resizeObserver.disconnect();
});
useTouchMove(this.gridOverlay, this.props.onGridMoved, () => {
const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
return scrollY > 0;
+213 -193
View File
@@ -280,32 +280,6 @@
}
}
}
function makePropSetter(name) {
return function setProp(value) {
// support 0, fallback to empty string for other falsy values
this[name] = value === 0 ? 0 : value ? value.valueOf() : "";
};
}
function isProp(tag, key) {
switch (tag) {
case "input":
return (key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "disabled");
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
function createEventHandler(rawEvent) {
const eventName = rawEvent.split(".")[0];
@@ -608,6 +582,12 @@
const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get;
const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get;
const NO_OP = () => { };
function makePropSetter(name) {
return function setProp(value) {
// support 0, fallback to empty string for other falsy values
this[name] = value === 0 ? 0 : value ? value.valueOf() : "";
};
}
const cache$1 = {};
/**
* Compiling blocks is a multi-step process:
@@ -725,6 +705,15 @@
tag: tagName,
});
}
else if (attrName.startsWith("block-property-")) {
const idx = parseInt(attrName.slice(15), 10);
info.push({
type: "property",
idx,
name: attrValue,
tag: tagName,
});
}
else if (attrName === "block-attributes") {
info.push({
type: "attributes",
@@ -871,16 +860,22 @@
};
}
break;
case "property": {
const refIdx = info.refIdx;
const setProp = makePropSetter(info.name);
ctx.locations.push({
idx: info.idx,
refIdx,
setData: setProp,
updateData: setProp,
});
break;
}
case "attribute": {
const refIdx = info.refIdx;
let updater;
let setter;
if (isProp(info.tag, info.name)) {
const setProp = makePropSetter(info.name);
setter = setProp;
updater = setProp;
}
else if (info.name === "class") {
if (info.name === "class") {
setter = setClass;
updater = updateClass;
}
@@ -2485,6 +2480,18 @@
this.fiber = null;
}
}
/**
* Sets a ref to a given HTMLElement.
*
* @param name the name of the ref to set
* @param el the HTMLElement to set the ref to. The ref is not set if the el
* is null, but useRef will not return elements that are not in the DOM
*/
setRef(name, el) {
if (el) {
this.refs[name] = el;
}
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
@@ -3020,45 +3027,6 @@
}
return toggler(safeKey, block);
}
let boundFunctions = new WeakMap();
const WeakMapGet = WeakMap.prototype.get;
const WeakMapSet = WeakMap.prototype.set;
function bind(component, fn) {
let boundFnMap = WeakMapGet.call(boundFunctions, component);
if (!boundFnMap) {
boundFnMap = new WeakMap();
WeakMapSet.call(boundFunctions, component, boundFnMap);
}
let boundFn = WeakMapGet.call(boundFnMap, fn);
if (!boundFn) {
boundFn = fn.bind(component);
WeakMapSet.call(boundFnMap, fn, boundFn);
}
return boundFn;
}
function multiRefSetter(refs, name) {
let count = 0;
return (el) => {
if (el) {
count++;
if (count > 1) {
throw new OwlError("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
refs[name] = el;
}
};
}
function singleRefSetter(refs, name) {
let _el = null;
return (el) => {
if (el || refs[name] === _el) {
refs[name] = el;
_el = el;
}
};
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
@@ -3097,6 +3065,16 @@
throw new OwlError(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
}
}
function makeRefWrapper(node) {
let refNames = new Set();
return (name, fn) => {
if (refNames.has(name)) {
throw new OwlError(`Cannot set the same ref more than once in the same component, ref "${name}" was set multiple times in ${node.name}`);
}
refNames.add(name);
return fn;
};
}
const helpers = {
withDefault,
zero: Symbol("zero"),
@@ -3106,17 +3084,15 @@
withKey,
prepareList,
setContextValue,
multiRefSetter,
singleRefSetter,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
bind,
createCatcher,
markRaw,
OwlError,
makeRefWrapper,
};
const bdom = { text, createBlock, list, multi, html, toggler, comment };
@@ -3543,6 +3519,7 @@
return replaceDynamicParts(s, compileExpr);
}
const whitespaceRE = /\s+/g;
// using a non-html document so that <inner/outer>HTML serializes as XML instead
// of HTML (as we will parse it as xml later)
const xmlDoc = document.implementation.createDocument(null, null, null);
@@ -3552,6 +3529,27 @@
nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
return prefix + nextDataIds[prefix];
}
function isProp(tag, key) {
switch (tag) {
case "input":
return (key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "readOnly" ||
key === "disabled");
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "readOnly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
// -----------------------------------------------------------------------------
// BlockDescription
// -----------------------------------------------------------------------------
@@ -3627,10 +3625,8 @@
this.code = [];
this.hasRoot = false;
this.hasCache = false;
this.hasRef = false;
// maps ref name to [id, expr]
this.refInfo = {};
this.shouldProtectScope = false;
this.hasRefWrapper = false;
this.name = name;
this.on = on || null;
}
@@ -3646,17 +3642,13 @@
generateCode() {
let result = [];
result.push(`function ${this.name}(ctx, node, key = "") {`);
if (this.hasRef) {
result.push(` const refs = this.__owl__.refs;`);
for (let name in this.refInfo) {
const [id, expr] = this.refInfo[name];
result.push(` const ${id} = ${expr};`);
}
}
if (this.shouldProtectScope) {
result.push(` ctx = Object.create(ctx);`);
result.push(` ctx[isBoundary] = 1`);
}
if (this.hasRefWrapper) {
result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
}
if (this.hasCache) {
result.push(` let cache = ctx.cache || {};`);
result.push(` let nextCache = ctx.cache = {};`);
@@ -3938,6 +3930,9 @@
const match = translationRE.exec(value);
value = match[1] + this.translateFn(match[2]) + match[3];
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
}
if (!block || forceNewBlock) {
block = this.createBlock(block, "text", ctx);
this.insertBlock(`text(\`${value}\`)`, block, {
@@ -4002,21 +3997,29 @@
attrName = key === "t-att" ? null : key.slice(6);
expr = compileExpr(ast.attrs[key]);
if (attrName && isProp(ast.tag, attrName)) {
if (attrName === "readonly") {
// the property has a different name than the attribute
attrName = "readOnly";
}
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
if (attrName === "value") {
// When the expression is falsy, fall back to an empty string
expr = `new String((${expr}) || "")`;
// When the expression is falsy (except 0), fall back to an empty string
expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
}
else {
expr = `new Boolean(${expr})`;
}
}
const idx = block.insertData(expr, "attr");
if (key === "t-att") {
attrs[`block-attributes`] = String(idx);
const idx = block.insertData(expr, "prop");
attrs[`block-property-${idx}`] = attrName;
}
else {
attrs[`block-attribute-${idx}`] = attrName;
const idx = block.insertData(expr, "attr");
if (key === "t-att") {
attrs[`block-attributes`] = String(idx);
}
else {
attrs[`block-attribute-${idx}`] = attrName;
}
}
}
else if (this.translatableAttributes.includes(key)) {
@@ -4053,8 +4056,8 @@
targetExpr = compileExpr(dynamicTgExpr);
}
}
idx = block.insertData(`${fullExpression} === ${targetExpr}`, "attr");
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
idx = block.insertData(`${fullExpression} === ${targetExpr}`, "prop");
attrs[`block-property-${idx}`] = specialInitTargetAttr;
}
else if (hasDynamicChildren) {
const bValueId = generateId("bValue");
@@ -4062,8 +4065,8 @@
this.define(tModelSelectedExpr, fullExpression);
}
else {
idx = block.insertData(`${fullExpression}`, "attr");
attrs[`block-attribute-${idx}`] = targetAttr;
idx = block.insertData(`${fullExpression}`, "prop");
attrs[`block-property-${idx}`] = targetAttr;
}
this.helpers.add("toNumber");
let valueCode = `ev.target.${targetAttr}`;
@@ -4081,32 +4084,21 @@
}
// t-ref
if (ast.ref) {
this.target.hasRef = true;
if (this.dev) {
this.helpers.add("makeRefWrapper");
this.target.hasRefWrapper = true;
}
const isDynamic = INTERP_REGEXP.test(ast.ref);
let name = `\`${ast.ref}\``;
if (isDynamic) {
this.helpers.add("singleRefSetter");
const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
const idx = block.insertData(`singleRefSetter(refs, ${str})`, "ref");
attrs["block-ref"] = String(idx);
name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
}
else {
let name = ast.ref;
if (name in this.target.refInfo) {
// ref has already been defined
this.helpers.add("multiRefSetter");
const info = this.target.refInfo[name];
const index = block.data.push(info[0]) - 1;
attrs["block-ref"] = String(index);
info[1] = `multiRefSetter(refs, \`${name}\`)`;
}
else {
let id = generateId("ref");
this.helpers.add("singleRefSetter");
this.target.refInfo[name] = [id, `singleRefSetter(refs, \`${name}\`)`];
const index = block.data.push(id) - 1;
attrs["block-ref"] = String(index);
}
let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
if (this.dev) {
setRefStr = `refWrapper(${name}, ${setRefStr})`;
}
const idx = block.insertData(setRefStr, "ref");
attrs["block-ref"] = String(idx);
}
const dom = xmlDoc.createElement(ast.tag);
for (const [attr, val] of Object.entries(attrs)) {
@@ -4129,6 +4121,7 @@
tKeyExpr: ctx.tKeyExpr,
nameSpace,
tModelSelectedExpr,
inPreTag: ctx.inPreTag || ast.tag === "pre",
});
this.compileAST(child, subCtx);
}
@@ -4513,13 +4506,15 @@
value = this.captureExpression(value);
if (name.includes(".")) {
let [_name, suffix] = name.split(".");
if (suffix === "bind") {
this.helpers.add("bind");
name = _name;
value = `bind(this, ${value || undefined})`;
}
else {
throw new OwlError("Invalid prop suffix");
name = _name;
switch (suffix) {
case "bind":
value = `(${value}).bind(this)`;
break;
case "alike":
break;
default:
throw new OwlError("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
@@ -4606,9 +4601,16 @@
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
}
let id = generateId("comp");
const propList = [];
for (let p in ast.props || {}) {
let [name, suffix] = p.split(".");
if (!suffix) {
propList.push(`"${name}"`);
}
}
this.staticDefs.push({
id,
expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${!ast.props && !ast.dynamicProps})`,
expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
});
if (ast.isDynamic) {
// If the component class changes, this can cause delayed renders to go
@@ -4786,15 +4788,11 @@
// Text and Comment Nodes
// -----------------------------------------------------------------------------
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node, ctx) {
if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) {
return null;
}
value = value.replace(whitespaceRE, " ");
if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
return null;
}
return { type: 0 /* Text */, value };
}
@@ -5482,50 +5480,8 @@
return new Function("app, bdom, helpers", code);
}
const mainEventHandler = (data, ev, currentTarget) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
data = _data;
let stopped = false;
if (modifiers.length) {
let selfMode = false;
const isSelf = ev.target === currentTarget;
for (const mod of modifiers) {
switch (mod) {
case "self":
selfMode = true;
if (isSelf) {
continue;
}
else {
return stopped;
}
case "prevent":
if ((selfMode && isSelf) || !selfMode)
ev.preventDefault();
continue;
case "stop":
if ((selfMode && isSelf) || !selfMode)
ev.stopPropagation();
stopped = true;
continue;
}
}
}
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
// as expected when there is a handler expression that evaluates to a falsy value
if (Object.hasOwnProperty.call(data, 0)) {
const handler = data[0];
if (typeof handler !== "function") {
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
}
let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === 1 /* MOUNTED */ : true) {
handler.call(node ? node.component : null, ev);
}
}
return stopped;
};
// do not modify manually. This value is updated by the release script.
const version = "2.0.9";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5610,6 +5566,7 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
super(config);
this.scheduler = new Scheduler();
this.root = null;
this.name = config.name || "";
this.Root = Root;
window.__OWL_DEVTOOLS__.apps.add(this);
if (config.test) {
@@ -5670,21 +5627,36 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
}
window.__OWL_DEVTOOLS__.apps.delete(this);
}
createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, hasNoProp) {
createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, propList) {
const isDynamic = !isStatic;
function _arePropsDifferent(props1, props2) {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
let arePropsDifferent;
const hasNoProp = propList.length === 0;
if (hasSlotsProp) {
arePropsDifferent = (_1, _2) => true;
}
else if (hasDynamicPropList) {
arePropsDifferent = function (props1, props2) {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return Object.keys(props1).length !== Object.keys(props2).length;
};
}
else if (hasNoProp) {
arePropsDifferent = (_1, _2) => false;
}
else {
arePropsDifferent = function (props1, props2) {
for (let p of propList) {
if (props1[p] !== props2[p]) {
return true;
}
}
return false;
};
}
const arePropsDifferent = hasSlotsProp
? (_1, _2) => true
: hasNoProp
? (_1, _2) => false
: _arePropsDifferent;
const updateAndRender = ComponentNode.prototype.updateAndRender;
const initiateRender = ComponentNode.prototype.initiateRender;
return (props, key, ctx, parent, C) => {
@@ -5728,10 +5700,56 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
}
}
App.validateTarget = validateTarget;
App.version = version;
async function mount(C, target, config = {}) {
return new App(C, config).mount(target, config);
}
const mainEventHandler = (data, ev, currentTarget) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
data = _data;
let stopped = false;
if (modifiers.length) {
let selfMode = false;
const isSelf = ev.target === currentTarget;
for (const mod of modifiers) {
switch (mod) {
case "self":
selfMode = true;
if (isSelf) {
continue;
}
else {
return stopped;
}
case "prevent":
if ((selfMode && isSelf) || !selfMode)
ev.preventDefault();
continue;
case "stop":
if ((selfMode && isSelf) || !selfMode)
ev.stopPropagation();
stopped = true;
continue;
}
}
}
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
// as expected when there is a handler expression that evaluates to a falsy value
if (Object.hasOwnProperty.call(data, 0)) {
const handler = data[0];
if (typeof handler !== "function") {
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
}
let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === 1 /* MOUNTED */ : true) {
handler.call(node ? node.component : null, ev);
}
}
return stopped;
};
function status(component) {
switch (component.__owl__.status) {
case 0 /* NEW */:
@@ -5755,7 +5773,8 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
const refs = node.refs;
return {
get el() {
return refs[name] || null;
const el = refs[name];
return (el === null || el === void 0 ? void 0 : el.ownerDocument.contains(el)) ? el : null;
},
};
}
@@ -5860,7 +5879,9 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
html,
comment,
};
const __info__ = {};
const __info__ = {
version: App.version,
};
TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {
return compile(template, {
@@ -5909,9 +5930,8 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
Object.defineProperty(exports, '__esModule', { value: true });
__info__.version = '2.0.7';
__info__.date = '2023-02-20T08:44:56.632Z';
__info__.hash = '276c8a0';
__info__.date = '2023-03-13T09:56:54.968Z';
__info__.hash = '8893e02';
__info__.url = 'https://github.com/odoo/owl';
@@ -9,6 +9,7 @@ import {
Component,
EventBus,
onWillStart,
status,
useEffect,
useExternalListener,
useRef,
@@ -192,6 +193,9 @@ export class Dropdown extends Component {
async changeStateAndNotify(stateSlice) {
if (stateSlice.open && this.props.beforeOpen) {
await this.props.beforeOpen();
if (status(this) === "destroyed") {
return;
}
}
// Update the state
Object.assign(this.state, stateSlice);
@@ -252,7 +256,7 @@ export class Dropdown extends Component {
* @param {DropdownStateChangedPayload} args
*/
onDropdownStateChanged(args) {
if (this.rootRef.el.contains(args.emitter.rootRef.el)) {
if (!this.rootRef.el || this.rootRef.el.contains(args.emitter.rootRef.el)) {
// Do not listen to events emitted by self or children
return;
}
@@ -203,7 +203,11 @@ export class NameAndSignature extends Component {
let height = 0;
const ratio = image.width / image.height;
const canvas = this.signatureRef.el.querySelector("canvas");
const signatureEl = this.signatureRef.el;
if (!signatureEl) {
return;
}
const canvas = signatureEl.querySelector("canvas");
const context = canvas.getContext("2d");
if (image.width / canvas.width > image.height / canvas.height) {
@@ -159,10 +159,10 @@ odoo.define('web.OwlCompatibility', function (require) {
}
onWillUnmount() {
this.removeEl();
if (this.widget && this.widget.on_detach_callback) {
this.widget.on_detach_callback();
}
this.removeEl();
}
removeEl() {
@@ -29,7 +29,7 @@
<t t-name="web.Many2ManyTagsFieldColorListPopover" owl="1">
<div class="o_tag_popover m-2">
<ColorList colors="props.colors" forceExpanded="true" onColorSelected="(id) => props.switchTagColor(id, props.tag)"/>
<CheckBox className="'pt-2'" value="props.tag.colorIndex === 0" onChange.bind="(isChecked) => props.onTagVisibilityChange(isChecked, props.tag)">Hide in kanban</CheckBox>
<CheckBox className="'pt-2'" value="props.tag.colorIndex === 0" onChange.alike="(isChecked) => props.onTagVisibilityChange(isChecked, props.tag)">Hide in kanban</CheckBox>
</div>
</t>
@@ -225,7 +225,7 @@
t-on-touchmove="() => this.onRowTouchMove(record)"
>
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" t-if="hasSelectors" class="o_list_record_selector user-select-none" t-on-click.stop="() => this.toggleRecordSelection(record)" tabindex="-1">
<CheckBox disabled="!!props.list.editedRecord or props.list.model.useSampleModel" value="record.selected" onChange.bind="() => this.toggleRecordSelection(record)" />
<CheckBox disabled="!!props.list.editedRecord or props.list.model.useSampleModel" value="record.selected" onChange.alike="() => this.toggleRecordSelection(record)" />
</td>
<t t-foreach="getColumns(record)" t-as="column" t-key="column.id">
<t t-if="column.type === 'field'">
@@ -130,7 +130,8 @@ export function useViewButtons(model, ref, options = {}) {
function getEl() {
if (env.inDialog) {
return ref.el.closest(".modal");
const el = ref.el;
return el ? el.closest(".modal") : null;
} else {
return ref.el;
}