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 @@
-
+
diff --git a/addons/web/static/tests/core/popover/popover_hook_tests.js b/addons/web/static/tests/core/popover/popover_hook_tests.js index dda5f7889bd..841bf4ad005 100644 --- a/addons/web/static/tests/core/popover/popover_hook_tests.js +++ b/addons/web/static/tests/core/popover/popover_hook_tests.js @@ -48,17 +48,17 @@ QUnit.test("close popover when component is unmounted", async (assert) => { class CompWithPopover extends Component { setup() { - this.popover = usePopover(); + this.popover = usePopover(Comp); } } CompWithPopover.template = xml`
`; const comp1 = await mount(CompWithPopover, target, { env }); - comp1.popover.add(popoverTarget, Comp, { id: "comp1" }); + comp1.popover.open(popoverTarget, { id: "comp1" }); await nextTick(); const comp2 = await mount(CompWithPopover, target, { env }); - comp2.popover.add(popoverTarget, Comp, { id: "comp2" }); + comp2.popover.open(popoverTarget, { id: "comp2" }); await nextTick(); assert.containsN(target, ".o_popover", 2);