diff --git a/addons/account/static/src/components/account_payment_field/account_payment_field.js b/addons/account/static/src/components/account_payment_field/account_payment_field.js
index 0a8646effde..99ae935ae1f 100644
--- a/addons/account/static/src/components/account_payment_field/account_payment_field.js
+++ b/addons/account/static/src/components/account_payment_field/account_payment_field.js
@@ -15,7 +15,8 @@ AccountPaymentPopOver.template = "account.AccountPaymentPopOver";
export class AccountPaymentField extends Component {
setup() {
- this.popover = usePopover();
+ const position = localization.direction === "rtl" ? "bottom" : "left";
+ this.popover = usePopover(AccountPaymentPopOver, { position });
this.orm = useService("orm");
this.action = useService("action");
@@ -45,28 +46,12 @@ export class AccountPaymentField extends Component {
}
onInfoClick(ev, idx) {
- if (this.popoverCloseFn) {
- this.closePopover();
- }
- this.popoverCloseFn = this.popover.add(
- ev.currentTarget,
- AccountPaymentPopOver,
- {
- title: this.env._t("Journal Entry Info"),
- ...this.lines[idx],
- _onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
- _onOpenMove: this.openMove.bind(this),
- onClose: this.closePopover,
- },
- {
- position: localization.direction === "rtl" ? "bottom" : "left",
- },
- );
- }
-
- closePopover() {
- this.popoverCloseFn();
- this.popoverCloseFn = null;
+ this.popover.open(ev.currentTarget, {
+ title: this.env._t("Journal Entry Info"),
+ ...this.lines[idx],
+ _onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
+ _onOpenMove: this.openMove.bind(this),
+ });
}
async assignOutstandingCredit(id) {
@@ -76,7 +61,7 @@ export class AccountPaymentField extends Component {
}
async removeMoveReconcile(moveId, partialId) {
- this.closePopover();
+ this.popover.close();
await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});
await this.props.record.model.root.load();
this.props.record.model.notify();
diff --git a/addons/hr_org_chart/static/src/fields/hr_org_chart.js b/addons/hr_org_chart/static/src/fields/hr_org_chart.js
index 41be89bfa82..795d1bc1423 100644
--- a/addons/hr_org_chart/static/src/fields/hr_org_chart.js
+++ b/addons/hr_org_chart/static/src/fields/hr_org_chart.js
@@ -7,23 +7,6 @@ import { onEmployeeSubRedirect } from './hooks';
const { Component, onWillStart, onWillRender, useState } = owl;
-function useUniquePopover() {
- const popover = usePopover();
- let remove = null;
- return Object.assign(Object.create(popover), {
- add(target, component, props, options) {
- if (remove) {
- remove();
- }
- remove = popover.add(target, component, props, options);
- return () => {
- remove();
- remove = null;
- };
- },
- });
-}
-
class HrOrgChartPopover extends Component {
async setup() {
super.setup();
@@ -56,7 +39,7 @@ export class HrOrgChart extends Component {
this.orm = useService('orm');
this.actionService = useService("action");
this.user = useService("user");
- this.popover = useUniquePopover();
+ this.popover = usePopover(HrOrgChartPopover);
this.state = useState({'employee_id': null});
this._onEmployeeSubRedirect = onEmployeeSubRedirect();
@@ -109,12 +92,7 @@ export class HrOrgChart extends Component {
}
_onOpenPopover(event, employee) {
- this.popover.add(
- event.currentTarget,
- HrOrgChartPopover,
- {employee},
- {closeOnClickAway: true}
- );
+ this.popover.open(event.currentTarget, { employee });
}
/**
diff --git a/addons/mail/static/src/core_ui/message.js b/addons/mail/static/src/core_ui/message.js
index f88adf30e7e..092bb84cf44 100644
--- a/addons/mail/static/src/core_ui/message.js
+++ b/addons/mail/static/src/core_ui/message.js
@@ -58,6 +58,7 @@ export class Message extends Component {
MessageReactions,
MessageSeenIndicator,
ImStatus,
+ Popover: MessageNotificationPopover,
RelativeTime,
};
static defaultProps = {
@@ -78,7 +79,7 @@ export class Message extends Component {
static template = "mail.Message";
setup() {
- this.popover = usePopover();
+ this.popover = usePopover(this.constructor.components.Popover, { position: "top" });
this.state = useState({
isEditing: false,
isHovered: false,
@@ -456,12 +457,7 @@ export class Message extends Component {
}
onClickNotificationIcon(ev) {
- this.popover.add(
- ev.target,
- MessageNotificationPopover,
- { message: this.message },
- { position: "top" }
- );
+ this.popover.open(ev.target, { message: this.message });
}
onClickFailure(ev) {
diff --git a/addons/mail/static/src/discuss/discuss.js b/addons/mail/static/src/discuss/discuss.js
index dd6bd6d7c0a..506d82e30a5 100644
--- a/addons/mail/static/src/discuss/discuss.js
+++ b/addons/mail/static/src/discuss/discuss.js
@@ -60,8 +60,9 @@ export class Discuss extends Component {
this.messageEdition = useMessageEdition();
this.messageToReplyTo = useMessageToReplyTo();
this.contentRef = useRef("content");
- this.popover = usePopover();
- this.closePopover = null;
+ this.popover = usePopover(ChannelInvitation, {
+ onClose: () => (this.state.isAddingUsers = false),
+ });
this.addUsersRef = useRef("addUsers");
this.state = useState({
activeMode: this.MODES.NONE,
@@ -120,23 +121,11 @@ export class Discuss extends Component {
}
toggleInviteForm() {
- if (this.closePopover) {
- this.closePopover();
- this.closePopover = null;
+ if (this.popover.isOpen) {
+ this.popover.close();
} else {
- const el = this.addUsersRef.el;
this.state.isAddingUsers = true;
- this.closePopover = this.popover.add(
- el,
- ChannelInvitation,
- { thread: this.thread },
- {
- onClose: () => {
- this.state.isAddingUsers = false;
- this.closePopover = null;
- },
- }
- );
+ this.popover.open(this.addUsersRef.el, { thread: this.thread });
}
}
diff --git a/addons/mail/static/src/emoji_picker/emoji_picker.js b/addons/mail/static/src/emoji_picker/emoji_picker.js
index 088c7f1862b..d831351ab7c 100644
--- a/addons/mail/static/src/emoji_picker/emoji_picker.js
+++ b/addons/mail/static/src/emoji_picker/emoji_picker.js
@@ -30,8 +30,7 @@ import { escapeRegExp } from "@web/core/utils/strings";
*/
export function useEmojiPicker(ref, props, options = {}) {
const targets = [];
- const popover = usePopover();
- let closePopover = false;
+ const popover = usePopover(EmojiPicker, { ...options, popoverClass: "o-fast-popover" });
props.storeScroll = {
scrollValue: 0,
set: (value) => {
@@ -59,20 +58,10 @@ export function useEmojiPicker(ref, props, options = {}) {
}
function toggle(ref, onSelect = props.onSelect) {
- if (closePopover) {
- closePopover();
- closePopover = false;
+ if (popover.isOpen) {
+ popover.close();
} else {
- closePopover = popover.add(
- ref.el,
- EmojiPicker,
- { ...props, onSelect },
- {
- ...options,
- onClose: () => (closePopover = false),
- popoverClass: "o-fast-popover",
- }
- );
+ popover.open(ref.el, { ...props, onSelect });
}
}
@@ -109,7 +98,7 @@ export function useEmojiPicker(ref, props, options = {}) {
return {
add,
get isOpen() {
- return Boolean(closePopover);
+ return popover.isOpen;
},
};
}
diff --git a/addons/mail/static/src/rtc/call_participant_card.js b/addons/mail/static/src/rtc/call_participant_card.js
index 722cc666859..e1e740a0050 100644
--- a/addons/mail/static/src/rtc/call_participant_card.js
+++ b/addons/mail/static/src/rtc/call_participant_card.js
@@ -17,11 +17,9 @@ export class CallParticipantCard extends Component {
static components = { CallParticipantVideo };
static template = "mail.CallParticipantCard";
- closePopover;
-
setup() {
this.contextMenuAnchorRef = useRef("contextMenuAnchor");
- this.popover = usePopover();
+ this.popover = usePopover(CallContextMenu);
this.rpc = useService("rpc");
this.rtc = useRtc();
this.store = useStore();
@@ -125,24 +123,15 @@ export class CallParticipantCard extends Component {
onContextMenu(ev) {
ev.preventDefault();
markEventHandled(ev, "CallParticipantCard.clickVolumeAnchor");
- if (this.closePopover) {
- this.closePopover();
- this.closePopover = undefined;
+ if (this.popover.isOpen) {
+ this.popover.close();
return;
}
if (!this.contextMenuAnchorRef?.el) {
return;
}
- this.closePopover = this.popover.add(
- this.contextMenuAnchorRef.el,
- CallContextMenu,
- {
- rtcSession: this.rtcSession,
- },
- {
- onClose: () => (this.closePopover = undefined),
- position: "bottom",
- }
- );
+ this.popover.open(this.contextMenuAnchorRef.el, {
+ rtcSession: this.rtcSession,
+ });
}
}
diff --git a/addons/mail/static/src/web/activity/activity.js b/addons/mail/static/src/web/activity/activity.js
index a790e517a48..e28132f4cab 100644
--- a/addons/mail/static/src/web/activity/activity.js
+++ b/addons/mail/static/src/web/activity/activity.js
@@ -41,7 +41,7 @@ export class Activity extends Component {
showDetails: false,
delay: computeDelay(this.props.data.date_deadline),
});
- this.popover = usePopover();
+ this.popover = usePopover(ActivityMarkAsDone, { position: "right" });
onMounted(() => {
this.updateDelayAtNight();
});
@@ -70,21 +70,15 @@ export class Activity extends Component {
}
async onClickMarkAsDone(ev) {
- if (this.closePopover) {
- this.closePopover();
- this.closePopover = undefined;
+ if (this.popover.isOpen) {
+ this.popover.close();
return;
}
- this.closePopover = this.popover.add(
- ev.currentTarget,
- ActivityMarkAsDone,
- {
- activity: this.props.data,
- hasHeader: true,
- reload: this.props.onUpdate,
- },
- { position: "right" }
- );
+ this.popover.open(ev.currentTarget, {
+ activity: this.props.data,
+ hasHeader: true,
+ reload: this.props.onUpdate,
+ });
}
async onFileUploaded(data) {
diff --git a/addons/mail/static/src/web/activity/activity_button.js b/addons/mail/static/src/web/activity/activity_button.js
index 9691ddd3402..4b43f3ac32e 100644
--- a/addons/mail/static/src/web/activity/activity_button.js
+++ b/addons/mail/static/src/web/activity/activity_button.js
@@ -11,9 +11,8 @@ export class ActivityButton extends Component {
static template = "mail.ActivityButton";
setup() {
- this.popover = usePopover();
+ this.popover = usePopover(ActivityListPopover, { position: "bottom-start" });
this.buttonRef = useRef("button");
- this.closePopover = undefined;
}
get buttonClass() {
@@ -53,26 +52,17 @@ export class ActivityButton extends Component {
}
onClick() {
- if (this.closePopover) {
- this.closePopover();
- this.closePopover = undefined;
+ if (this.popover.isOpen) {
+ this.popover.close();
} else {
- this.closePopover = this.popover.add(
- this.buttonRef.el,
- ActivityListPopover,
- {
- activityIds: this.props.record.data.activity_ids.currentIds,
- onActivityChanged: () => {
- this.props.record.model.load({ resId: this.props.record.resId });
- },
- resId: this.props.record.resId,
- resModel: this.props.record.resModel,
+ this.popover.open(this.buttonRef.el, {
+ activityIds: this.props.record.data.activity_ids.currentIds,
+ onActivityChanged: () => {
+ this.props.record.model.load({ resId: this.props.record.resId });
},
- {
- onClose: () => (this.closePopover = undefined),
- position: "bottom", // should be "bottom-start" but not supported for some reason
- }
- );
+ resId: this.props.record.resId,
+ resModel: this.props.record.resModel,
+ });
}
}
}
diff --git a/addons/mail/static/src/web/activity/activity_cell.js b/addons/mail/static/src/web/activity/activity_cell.js
index b72ed875ac4..dd0b40335a6 100644
--- a/addons/mail/static/src/web/activity/activity_cell.js
+++ b/addons/mail/static/src/web/activity/activity_cell.js
@@ -21,7 +21,7 @@ export class ActivityCell extends Component {
static template = "mail.ActivityCell";
setup() {
- this.popover = usePopover();
+ this.popover = usePopover(ActivityListPopover, { position: "bottom-start" });
this.contentRef = useRef("content");
}
@@ -43,27 +43,18 @@ export class ActivityCell extends Component {
}
onClick() {
- if (this.closePopover) {
- this.closePopover();
- this.closePopover = undefined;
+ if (this.popover.isOpen) {
+ this.popover.close();
} else {
- this.closePopover = this.popover.add(
- this.contentRef.el,
- ActivityListPopover,
- {
- activityIds: this.props.activityIds,
- defaultActivityTypeId: this.props.activityTypeId,
- onActivityChanged: () => {
- this.props.reloadFunc();
- },
- resId: this.props.resId,
- resModel: this.props.resModel,
+ this.popover.open(this.contentRef.el, {
+ activityIds: this.props.activityIds,
+ defaultActivityTypeId: this.props.activityTypeId,
+ onActivityChanged: () => {
+ this.props.reloadFunc();
},
- {
- onClose: () => (this.closePopover = undefined),
- position: "bottom", // should be "bottom-start" but not supported for some reason
- }
- );
+ resId: this.props.resId,
+ resModel: this.props.resModel,
+ });
}
}
}
diff --git a/addons/sale_stock/static/src/widgets/qty_at_date_widget.js b/addons/sale_stock/static/src/widgets/qty_at_date_widget.js
index ccc7b46df22..be3893c32ea 100644
--- a/addons/sale_stock/static/src/widgets/qty_at_date_widget.js
+++ b/addons/sale_stock/static/src/widgets/qty_at_date_widget.js
@@ -6,7 +6,7 @@ import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { usePopover } from "@web/core/popover/popover_hook";
-const { Component, EventBus, onWillRender } = owl;
+const { Component, onWillRender } = owl;
export class QtyAtDatePopover extends Component {
setup() {
@@ -30,9 +30,7 @@ QtyAtDatePopover.template = "sale_stock.QtyDetailPopOver";
export class QtyAtDateWidget extends Component {
setup() {
- this.bus = new EventBus();
- this.popover = usePopover();
- this.closePopover = null;
+ this.popover = usePopover(this.constructor.components.Popover, { position: "top" });
this.calcData = {};
onWillRender(() => {
this.initCalcData();
@@ -74,15 +72,10 @@ export class QtyAtDateWidget extends Component {
showPopup(ev) {
this.updateCalcData();
- this.closePopover = this.popover.add(
- ev.currentTarget,
- this.constructor.components.Popover,
- {bus: this.bus, record: this.props.record, calcData: this.calcData},
- {
- position: 'top',
- }
- );
- this.bus.addEventListener('close-popover', this.closePopover);
+ this.popover.open(ev.currentTarget, {
+ record: this.props.record,
+ calcData: this.calcData,
+ });
}
}
diff --git a/addons/snailmail/static/src/core_ui/message_patch.js b/addons/snailmail/static/src/core_ui/message_patch.js
index 2667f862260..aa6ac224d24 100644
--- a/addons/snailmail/static/src/core_ui/message_patch.js
+++ b/addons/snailmail/static/src/core_ui/message_patch.js
@@ -54,22 +54,10 @@ patch(Message.prototype, "snailmail", {
},
});
},
-
- onClickNotificationIcon(ev) {
- if (this.message.type === "snailmail") {
- this.popover.add(
- ev.target,
- SnailmailNotificationPopover,
- { message: this.message },
- { position: "top" }
- );
- } else {
- this._super(...arguments);
- }
- },
});
Message.components = {
...Message.components,
+ Popover: SnailmailNotificationPopover,
SnailmailError,
};
diff --git a/addons/snailmail/static/src/core_ui/snailmail_notification_popover.xml b/addons/snailmail/static/src/core_ui/snailmail_notification_popover.xml
index 55d6864c212..b1518f87efe 100644
--- a/addons/snailmail/static/src/core_ui/snailmail_notification_popover.xml
+++ b/addons/snailmail/static/src/core_ui/snailmail_notification_popover.xml
@@ -1,11 +1,16 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+ $0
+
diff --git a/addons/stock/static/src/widgets/popover_widget.js b/addons/stock/static/src/widgets/popover_widget.js
index 6d9c1ee88e0..164bc12e73d 100644
--- a/addons/stock/static/src/widgets/popover_widget.js
+++ b/addons/stock/static/src/widgets/popover_widget.js
@@ -26,23 +26,16 @@ PopoverComponent.template = 'stock.popoverContent';
export class PopoverWidgetField extends Component {
setup(){
- this.popover = usePopover();
- this.closePopover = null;
let fieldValue = this.props.record.data[this.props.name];
this.jsonValue = JSON.parse(fieldValue || "{}");
+ const position = this.jsonValue.position || "top";
+ this.popover = usePopover(this.constructor.components.Popover, { position });
this.color = this.jsonValue.color || 'text-primary';
this.icon = this.jsonValue.icon || 'fa-info-circle';
}
showPopup(ev){
- this.closePopover = this.popover.add(
- ev.currentTarget,
- this.constructor.components.Popover,
- {...this.jsonValue, record: this.props.record},
- {
- position: this.jsonValue.position || 'top',
- }
- );
+ this.popover.open(ev.currentTarget, { ...this.jsonValue, record: this.props.record });
}
}
diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.js b/addons/web/static/src/core/model_field_selector/model_field_selector.js
index f50c00ee9f7..55e2f9af2d4 100644
--- a/addons/web/static/src/core/model_field_selector/model_field_selector.js
+++ b/addons/web/static/src/core/model_field_selector/model_field_selector.js
@@ -1,14 +1,21 @@
/** @odoo-module **/
import { useModelField } from "./model_field_hook";
-import { useUniquePopover } from "./unique_popover_hook";
+import { usePopover } from "@web/core/popover/popover_hook";
import { ModelFieldSelectorPopover } from "./model_field_selector_popover";
import { Component, onWillStart, onWillUpdateProps, toRaw, useState } from "@odoo/owl";
export class ModelFieldSelector extends Component {
setup() {
- this.popover = useUniquePopover();
+ this.popover = usePopover(this.constructor.components.Popover, {
+ popoverClass: "o_popover_field_selector",
+ onClose: () => {
+ if (this.state.isDirty) {
+ this.props.update(this.state.fieldName, toRaw(this.state.chain));
+ }
+ },
+ });
this.modelField = useModelField();
this.state = useState({
chain: [],
@@ -54,28 +61,15 @@ export class ModelFieldSelector extends Component {
if (this.props.readonly) {
return;
}
- this.popover.add(
- ev.currentTarget,
- this.constructor.components.Popover,
- {
- chain: this.state.chain,
- update: this.update.bind(this),
- showSearchInput: this.props.showSearchInput,
- isDebugMode: this.props.isDebugMode,
- loadChain: this.loadChain.bind(this),
- filter: this.props.filter,
- followRelations: this.props.followRelations,
- },
- {
- closeOnClickAway: true,
- popoverClass: "o_popover_field_selector",
- onClose: () => {
- if (this.state.isDirty) {
- this.props.update(this.state.fieldName, toRaw(this.state.chain));
- }
- },
- }
- );
+ this.popover.open(ev.currentTarget, {
+ chain: this.state.chain,
+ update: this.update.bind(this),
+ showSearchInput: this.props.showSearchInput,
+ isDebugMode: this.props.isDebugMode,
+ loadChain: this.loadChain.bind(this),
+ filter: this.props.filter,
+ followRelations: this.props.followRelations,
+ });
}
}
diff --git a/addons/web/static/src/core/model_field_selector/unique_popover_hook.js b/addons/web/static/src/core/model_field_selector/unique_popover_hook.js
deleted file mode 100644
index a0cd7b9f206..00000000000
--- a/addons/web/static/src/core/model_field_selector/unique_popover_hook.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/** @odoo-module **/
-
-import { usePopover } from "@web/core/popover/popover_hook";
-
-export function useUniquePopover() {
- const popover = usePopover();
- let remove = null;
- return Object.assign(Object.create(popover), {
- add(target, component, props, options) {
- if (remove) {
- remove();
- }
- remove = popover.add(target, component, props, options);
- return () => {
- remove();
- remove = null;
- };
- },
- });
-}
diff --git a/addons/web/static/src/core/popover/popover_hook.js b/addons/web/static/src/core/popover/popover_hook.js
index 498a8911ee8..8e0585e14d3 100644
--- a/addons/web/static/src/core/popover/popover_hook.js
+++ b/addons/web/static/src/core/popover/popover_hook.js
@@ -4,39 +4,47 @@ import { useService } from "@web/core/utils/hooks";
import { onWillUnmount, status, useComponent } from "@odoo/owl";
-export function usePopover() {
- const removeFns = new Set();
- const service = useService("popover");
- const component = useComponent();
+/**
+ * @typedef PopoverHookReturnType
+ * @property {(target: string | HTMLElement, props: object) => void} open
+ * - Signals the manager to open the configured popover
+ * component on the target, with the given props.
+ * @property {() => void} close
+ * - Signals the manager to remove the popover.
+ * @property {boolean} isOpen
+ * - Whether the popover is currently open.
+ */
- onWillUnmount(function () {
- for (const removeFn of removeFns) {
- removeFn();
- }
- removeFns.clear();
- });
+/**
+ * Manages a component to be used as a popover.
+ *
+ * @param {typeof import("@odoo/owl").Component} component
+ * @param {import("@web/core/popover/popover_service").PopoverServiceAddOptions} [options]
+ * @returns {PopoverHookReturnType}
+ */
+export function usePopover(component, options = {}) {
+ let removeFn = null;
+ const popover = useService("popover");
+ const owner = useComponent();
+ function close() {
+ removeFn?.();
+ }
+ onWillUnmount(close);
return {
- /**
- * Signals the manager to add a popover.
- *
- * @param {string | HTMLElement} target
- * @param {typeof import("@odoo/owl").Component} Component
- * @param {object} props
- * @param {import("@web/core/popover/popover_service").PopoverServiceAddOptions} [options]
- * @returns {() => void}
- */
- add(target, Component, props, options = {}) {
+ open(target, props) {
+ close();
const newOptions = Object.create(options);
newOptions.onClose = function () {
- removeFns.delete(removeFn);
- if (options.onClose && status(component) !== "destroyed") {
- options.onClose();
+ removeFn = null;
+ if (status(owner) !== "destroyed") {
+ options.onClose?.();
}
};
-
- const removeFn = service.add(target, Component, props, newOptions);
- removeFns.add(removeFn);
- return removeFn;
+ removeFn = popover.add(target, component, props, newOptions);
+ },
+ close,
+ get isOpen() {
+ return Boolean(removeFn);
},
};
}
diff --git a/addons/web/static/src/views/calendar/hooks.js b/addons/web/static/src/views/calendar/hooks.js
index b45a2467d06..ca0d814653a 100644
--- a/addons/web/static/src/views/calendar/hooks.js
+++ b/addons/web/static/src/views/calendar/hooks.js
@@ -17,9 +17,12 @@ import {
export function useCalendarPopover(component) {
const owner = useComponent();
- const popover = usePopover();
+ let popoverClass = "";
+ const popoverOptions = { position: "right", onClose: cleanup };
+ Object.defineProperty(popoverOptions, "popoverClass", { get: () => popoverClass });
+ const popover = usePopover(component, popoverOptions);
const dialog = useService("dialog");
- let remove = null;
+ let removeDialog = null;
let fcPopover;
useExternalListener(
window,
@@ -34,27 +37,23 @@ export function useCalendarPopover(component) {
);
function cleanup() {
fcPopover = null;
- remove = null;
+ removeDialog = null;
}
function close() {
- if (remove) {
- remove();
- }
+ removeDialog?.();
+ popover.close();
cleanup();
}
return {
close,
- open(target, props, popoverClass) {
- close();
+ open(target, props, popoverClassToUse) {
fcPopover = target.closest(".fc-popover");
if (owner.env.isSmall) {
- remove = dialog.add(component, props, { onClose: cleanup });
+ close();
+ removeDialog = dialog.add(component, props, { onClose: cleanup });
} else {
- remove = popover.add(target, component, props, {
- popoverClass,
- position: "right",
- onClose: cleanup,
- });
+ popoverClass = popoverClassToUse;
+ popover.open(target, props);
}
},
};
diff --git a/addons/web/static/src/views/fields/copy_clipboard/copy_button.js b/addons/web/static/src/views/fields/copy_clipboard/copy_button.js
index a93ad6479c3..98aff60eea3 100644
--- a/addons/web/static/src/views/fields/copy_clipboard/copy_button.js
+++ b/addons/web/static/src/views/fields/copy_clipboard/copy_button.js
@@ -2,9 +2,9 @@
import { browser } from "@web/core/browser/browser";
import { Tooltip } from "@web/core/tooltip/tooltip";
-import { useService } from "@web/core/utils/hooks";
-
+import { usePopover } from "@web/core/popover/popover_hook";
import { Component, useRef } from "@odoo/owl";
+
export class CopyButton extends Component {
static template = "web.CopyButton";
static props = {
@@ -17,16 +17,12 @@ export class CopyButton extends Component {
setup() {
this.button = useRef("button");
- this.popover = useService("popover");
+ this.popover = usePopover(Tooltip);
}
showTooltip() {
- const closeTooltip = this.popover.add(this.button.el, Tooltip, {
- tooltip: this.props.successText,
- });
- browser.setTimeout(() => {
- closeTooltip();
- }, 800);
+ this.popover.open(this.button.el, { tooltip: this.props.successText });
+ browser.setTimeout(this.popover.close, 800);
}
async onClick() {
@@ -43,7 +39,7 @@ export class CopyButton extends Component {
}
try {
await write(this.props.content);
- } catch(error) {
+ } catch (error) {
return browser.console.warn(error);
}
this.showTooltip();
diff --git a/addons/web/static/src/views/fields/dynamic_placeholder_hook.js b/addons/web/static/src/views/fields/dynamic_placeholder_hook.js
index fe0b9abb59f..c1d3e8dfabf 100644
--- a/addons/web/static/src/views/fields/dynamic_placeholder_hook.js
+++ b/addons/web/static/src/views/fields/dynamic_placeholder_hook.js
@@ -1,6 +1,6 @@
/** @odoo-module **/
-import { useUniquePopover } from "@web/core/model_field_selector/unique_popover_hook";
+import { usePopover } from "@web/core/popover/popover_hook";
import { useModelField } from "@web/core/model_field_selector/model_field_hook";
import { useService } from "@web/core/utils/hooks";
import { ModelFieldSelectorPopover } from "@web/core/model_field_selector/model_field_selector_popover";
@@ -10,7 +10,12 @@ export function useDynamicPlaceholder(elementRef) {
const TRIGGER_KEY = "#";
const ownerField = useComponent();
const triggerKeyReplaceRegex = new RegExp(`${TRIGGER_KEY}$`);
- const popover = useUniquePopover();
+ let closeCallback;
+ let positionCallback;
+ const popoverOptions = {};
+ Object.defineProperty(popoverOptions, "onClose", { get: () => closeCallback });
+ Object.defineProperty(popoverOptions, "onPositioned", { get: () => positionCallback });
+ const popover = usePopover(ModelFieldSelectorPopover, popoverOptions);
const modelField = useModelField();
const notification = useService("notification");
@@ -54,12 +59,12 @@ export function useDynamicPlaceholder(elementRef) {
* placeholder string in the Input with or without a default text value.
*
* @public
- * @param {Object} options
+ * @param {Object} opts
* @param {function} options.validateCallback
* @param {function} options.closeCallback
* @param {function} [options.positionCallback]
*/
- async function open(options) {
+ async function open(opts) {
if (!model) {
return notification.add(
ownerField.env._t(
@@ -70,25 +75,18 @@ export function useDynamicPlaceholder(elementRef) {
}
dynamicPlaceholderChain = await modelField.loadChain(model, "");
- popover.add(
- elementRef?.el,
- ModelFieldSelectorPopover,
- {
- chain: dynamicPlaceholderChain,
- update: (chain) => { dynamicPlaceholderChain = chain; },
- validate: options.validateCallback,
- showSearchInput: true,
- isDebugMode: true,
- needDefaultValue: true,
- loadChain: modelField.loadChain,
- filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type),
- },
- {
- closeOnClickAway: true,
- onClose: options.closeCallback,
- onPositioned: options.positionCallback,
- }
- );
+ closeCallback = opts.closeCallback;
+ positionCallback = opts.positionCallback;
+ popover.open(elementRef?.el, {
+ chain: dynamicPlaceholderChain,
+ update: (chain) => (dynamicPlaceholderChain = chain),
+ validate: opts.validateCallback,
+ showSearchInput: true,
+ isDebugMode: true,
+ needDefaultValue: true,
+ loadChain: modelField.loadChain,
+ filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type),
+ });
}
async function onKeydown(ev) {
const element = elementRef?.el;
diff --git a/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js b/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
index f27a86179c2..5550fcdfa1a 100644
--- a/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
+++ b/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
@@ -59,7 +59,7 @@ export class Many2ManyTagsField extends Component {
setup() {
this.orm = useService("orm");
this.previousColorsMap = {};
- this.popover = usePopover();
+ this.popover = usePopover(this.constructor.components.Popover);
this.dialog = useService("dialog");
this.dialogClose = [];
@@ -312,37 +312,22 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
return props;
}
- closePopover() {
- this.popoverCloseFn();
- this.popoverCloseFn = null;
- }
-
onBadgeClick(ev, record) {
if (!this.props.canEditColor) {
return;
}
- const isClosed = !document.querySelector(".o_tag_popover");
- if (isClosed) {
- this.currentPopoverEl = null;
- }
- if (this.popoverCloseFn) {
- this.closePopover();
- }
- if (isClosed || this.currentPopoverEl !== ev.currentTarget) {
- this.currentPopoverEl = ev.currentTarget;
- this.popoverCloseFn = this.popover.add(
- ev.currentTarget,
- this.constructor.components.Popover,
- {
- colors: this.constructor.RECORD_COLORS,
- tag: {
- id: record.id,
- colorIndex: record.data[this.props.colorField],
- },
- switchTagColor: this.switchTagColor.bind(this),
- onTagVisibilityChange: this.onTagVisibilityChange.bind(this),
- }
- );
+ if (this.popover.isOpen) {
+ this.popover.close();
+ } else {
+ this.popover.open(ev.currentTarget, {
+ colors: this.constructor.RECORD_COLORS,
+ tag: {
+ id: record.id,
+ colorIndex: record.data[this.props.colorField],
+ },
+ switchTagColor: this.switchTagColor.bind(this),
+ onTagVisibilityChange: this.onTagVisibilityChange.bind(this),
+ });
}
}
@@ -357,7 +342,7 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
[this.props.colorField]: isHidden ? 0 : this.previousColorsMap[tagRecord.resId] || 1,
});
tagRecord.save();
- this.closePopover();
+ this.popover.close();
}
switchTagColor(colorIndex, tag) {
@@ -366,7 +351,7 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
);
tagRecord.update({ [this.props.colorField]: colorIndex });
tagRecord.save();
- this.closePopover();
+ this.popover.close();
}
}
diff --git a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js
index 8294ba4145a..e6dee65b111 100644
--- a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js
+++ b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js
@@ -101,39 +101,26 @@ export class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {
};
setup() {
super.setup();
- this.popover = usePopover();
+ this.popover = usePopover(Many2ManyTagsAvatarFieldPopover, {
+ popoverClass: "o_m2m_tags_avatar_field_popover",
+ closeOnClickAway: (target) => !target.closest(".modal"),
+ });
}
get visibleTagsCount() {
return this.props.itemsVisible;
}
- closePopover() {
- this.closePopoverFn();
- this.closePopoverFn = null;
- }
openPopover(ev) {
if (this.props.readonly) {
return;
}
- if (this.closePopoverFn) {
- this.closePopover();
- }
- this.closePopoverFn = this.popover.add(
- ev.currentTarget.parentElement,
- Many2ManyTagsAvatarFieldPopover,
- {
- ...this.props.popoverProps,
- readonly: false,
- canCreate: false,
- canCreateEdit: false,
- canQuickCreate: false,
- placeholder: this.env._t("Search users..."),
- },
- {
- position: "bottom",
- popoverClass: "o_m2m_tags_avatar_field_popover",
- closeOnClickAway: (target) => !target.closest(".modal"),
- }
- );
+ this.popover.open(ev.currentTarget.parentElement, {
+ ...this.props.popoverProps,
+ readonly: false,
+ canCreate: false,
+ canCreateEdit: false,
+ canQuickCreate: false,
+ placeholder: this.env._t("Search users..."),
+ });
}
get canDisplayQuickAssignAvatar() {
return !this.props.readonly && !(this.props.tags && this.otherTags.length);
diff --git a/addons/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js b/addons/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js
index bb8a03ec9bc..62bf6a60ebb 100644
--- a/addons/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js
+++ b/addons/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js
@@ -63,12 +63,10 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
};
setup() {
super.setup();
- this.popover = usePopover();
- }
-
- closePopover() {
- this.closePopoverFn();
- this.closePopoverFn = null;
+ this.popover = usePopover(Many2OneFieldPopover, {
+ popoverClass: "o_m2o_tags_avatar_field_popover",
+ closeOnClickAway: (target) => !target.closest(".modal"),
+ });
}
get popoverProps() {
const props = {
@@ -82,25 +80,13 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
if (!this.props.isEditable) {
return;
}
- if (this.closePopoverFn) {
- this.closePopover();
- }
- this.closePopoverFn = this.popover.add(
- ev.currentTarget,
- Many2OneFieldPopover,
- {
- ...this.popoverProps,
- canCreate: false,
- canCreateEdit: false,
- canQuickCreate: false,
- placeholder: this.env._t("Search user..."),
- },
- {
- position: "bottom",
- popoverClass: "o_m2o_tags_avatar_field_popover",
- closeOnClickAway: (target) => !target.closest(".modal"),
- }
- );
+ this.popover.open(ev.currentTarget, {
+ ...this.popoverProps,
+ canCreate: false,
+ canCreateEdit: false,
+ canQuickCreate: false,
+ placeholder: this.env._t("Search user..."),
+ });
}
}
diff --git a/addons/web/static/src/views/fields/properties/properties_field.js b/addons/web/static/src/views/fields/properties/properties_field.js
index e40c3693833..891fb4ea66d 100644
--- a/addons/web/static/src/views/fields/properties/properties_field.js
+++ b/addons/web/static/src/views/fields/properties/properties_field.js
@@ -37,7 +37,12 @@ export class PropertiesField extends Component {
this.orm = useService("orm");
this.user = useService("user");
this.dialogService = useService("dialog");
- this.popover = usePopover();
+ this.popover = usePopover(PropertyDefinition, {
+ closeOnClickAway: this.checkPopoverClose,
+ popoverClass: "o_property_field_popover",
+ position: "top",
+ onClose: () => this.onCloseCurrentPopover?.(),
+ });
this.propertiesRef = useRef("properties");
this.state = useState({
@@ -261,10 +266,7 @@ export class PropertiesField extends Component {
),
confirmLabel: _lt("Delete"),
confirm: () => {
- if (this.popoverCloseFn) {
- this.popoverCloseFn();
- this.popoverCloseFn = null;
- }
+ this.popover.close();
const propertiesDefinitions = this.propertiesList;
propertiesDefinitions.find(
(property) => property.name === propertyName
@@ -459,38 +461,31 @@ export class PropertiesField extends Component {
return propertyName;
};
- this.popoverCloseFn = this.popover.add(
- target,
- PropertyDefinition,
- {
- readonly: this.props.readonly || !this.state.canChangeDefinition,
- canChangeDefinition: this.state.canChangeDefinition,
- propertyDefinition: this.propertiesList.find(
- (property) => property.name === currentName(propertyName)
- ),
- context: this.props.context,
- onChange: this.onPropertyDefinitionChange.bind(this),
- onDelete: () => this.onPropertyDelete(currentName(propertyName)),
- onPropertyMove: (direction) =>
- this.onPropertyMove(currentName(propertyName), direction),
- isNewlyCreated: isNewlyCreated,
- propertyIndex: propertyIndex,
- propertiesSize: propertiesList.length,
- hideKanbanOption: this.props.hideKanbanOption,
- },
- {
- closeOnClickAway: this.checkPopoverClose,
- popoverClass: "o_property_field_popover",
- position: "top",
- onClose: () => {
- this.state.movedPropertyName = null;
- target.classList.remove("disabled");
- if (isNewlyCreated) {
- this._setDefaultPropertyValue(currentName(propertyName));
- }
- },
+ this.onCloseCurrentPopover = () => {
+ this.onCloseCurrentPopover = null;
+ this.state.movedPropertyName = null;
+ target.classList.remove("disabled");
+ if (isNewlyCreated) {
+ this._setDefaultPropertyValue(currentName(propertyName));
}
- );
+ };
+
+ this.popover.open(target, {
+ readonly: this.props.readonly || !this.state.canChangeDefinition,
+ canChangeDefinition: this.state.canChangeDefinition,
+ propertyDefinition: this.propertiesList.find(
+ (property) => property.name === currentName(propertyName)
+ ),
+ context: this.props.context,
+ onChange: this.onPropertyDefinitionChange.bind(this),
+ onDelete: () => this.onPropertyDelete(currentName(propertyName)),
+ onPropertyMove: (direction) =>
+ this.onPropertyMove(currentName(propertyName), direction),
+ isNewlyCreated: isNewlyCreated,
+ propertyIndex: propertyIndex,
+ propertiesSize: propertiesList.length,
+ hideKanbanOption: this.props.hideKanbanOption,
+ });
}
/**
diff --git a/addons/web/static/src/views/fields/properties/property_tags.js b/addons/web/static/src/views/fields/properties/property_tags.js
index 37102c7c1f4..d55ab0719bf 100644
--- a/addons/web/static/src/views/fields/properties/property_tags.js
+++ b/addons/web/static/src/views/fields/properties/property_tags.js
@@ -24,7 +24,7 @@ Object.assign(PropertyTagAutoComplete, { timeout: 0 });
export class PropertyTags extends Component {
setup() {
this.notification = useService("notification");
- this.popover = usePopover();
+ this.popover = usePopover(this.constructor.components.Popover);
}
/* --------------------------------------------------------
@@ -251,18 +251,11 @@ export class PropertyTags extends Component {
event.currentTarget.blur();
return;
}
- this.popoverCloseFn = this.popover.add(
- event.currentTarget,
- this.constructor.components.Popover,
- {
- colors: [...Array(ColorList.COLORS.length).keys()],
- tag: { id: tagId, colorIndex: tagColor },
- switchTagColor: this.onTagColorSwitch.bind(this),
- },
- {
- closeOnClickAway: true,
- }
- );
+ this.popover.open(event.currentTarget, {
+ colors: [...Array(ColorList.COLORS.length).keys()],
+ tag: { id: tagId, colorIndex: tagColor },
+ switchTagColor: this.onTagColorSwitch.bind(this),
+ });
}
/**
@@ -277,8 +270,7 @@ export class PropertyTags extends Component {
this.props.onTagsChange(availableTags);
// close the color popover
- this.popoverCloseFn();
- this.popoverCloseFn = null;
+ this.popover.close();
}
}
diff --git a/addons/web/static/src/views/kanban/kanban_header.js b/addons/web/static/src/views/kanban/kanban_header.js
index ccb681a9167..5290b7e00e7 100644
--- a/addons/web/static/src/views/kanban/kanban_header.js
+++ b/addons/web/static/src/views/kanban/kanban_header.js
@@ -35,33 +35,24 @@ export class KanbanHeader extends Component {
setup() {
this.dialog = useService("dialog");
this.orm = useService("orm");
-
this.rootRef = useRef("root");
- const titleRef = useRef("title");
+ this.popover = usePopover(KanbanHeaderTooltip);
+ this.onTitleMouseEnter = useDebounced(this.onTitleMouseEnter, 400);
+ }
- const popover = usePopover();
+ async onTitleMouseEnter(ev) {
+ if (!this.hasTooltip) {
+ return;
+ }
+ const tooltip = await this.loadTooltip();
+ if (tooltip.length) {
+ this.popover.open(ev.target, { tooltip });
+ }
+ }
- let shouldOpenTooltip;
- const debouncedOpenTooltip = useDebounced(async () => {
- shouldOpenTooltip = true;
- if (this.hasTooltip) {
- const tooltip = await this.loadTooltip();
- if (tooltip.length && shouldOpenTooltip) {
- this.closeTooltip = popover.add(titleRef.el, KanbanHeaderTooltip, {
- tooltip: tooltip,
- });
- shouldOpenTooltip = false;
- }
- }
- }, 400);
-
- this.openTooltip = () => {
- this.closeTooltip = () => {
- shouldOpenTooltip = false;
- debouncedOpenTooltip.cancel();
- };
- debouncedOpenTooltip();
- };
+ onTitleMouseLeave() {
+ this.onTitleMouseEnter.cancel();
+ this.popover.close();
}
// ------------------------------------------------------------------------
diff --git a/addons/web/static/src/views/kanban/kanban_header.xml b/addons/web/static/src/views/kanban/kanban_header.xml
index 68c72d588e0..d711a35a390 100644
--- a/addons/web/static/src/views/kanban/kanban_header.xml
+++ b/addons/web/static/src/views/kanban/kanban_header.xml
@@ -3,7 +3,7 @@