From b4b834c0a97ffb9a1a1310be4542ca93976da9fc Mon Sep 17 00:00:00 2001 From: Bruno Boi Date: Thu, 13 Apr 2023 12:43:53 +0000 Subject: [PATCH] [REF] web: simplify popover hook usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Before this commit** Almost all of the use cases of the `usePopover` hook are dealing with an abnormal amount of complexity: - they have to store the closing function of the popover, and sometimes keep track to its opening state, - you can find a duplication of a useUniquePopover hook, which is anyway the behavior that is always wanted for popover users. This is due to the fact that usePopover is just some kind of wrapper to the lower level popover service. **After the commit** - The usePopover now has a new API, which simplifies its usage. - The useUniquePopover hook implementations have been removed and this "unique" behavior is now merged into usePopover and has become the standard one (not configurable). - All of the use cases have been adapted, even the cases where components used directly the popover service (through `useService()`). See the adapted jsDoc around the hook in popover_hook.js closes odoo/odoo#118066 Related: odoo/enterprise#39654 Signed-off-by: Michaƫl Mattiello --- .../account_payment_field.js | 33 +++------ .../static/src/fields/hr_org_chart.js | 26 +------ addons/mail/static/src/core_ui/message.js | 10 +-- addons/mail/static/src/discuss/discuss.js | 23 ++----- .../static/src/emoji_picker/emoji_picker.js | 21 ++---- .../static/src/rtc/call_participant_card.js | 23 ++----- .../mail/static/src/web/activity/activity.js | 22 +++--- .../src/web/activity/activity_button.js | 30 +++------ .../static/src/web/activity/activity_cell.js | 31 +++------ .../static/src/widgets/qty_at_date_widget.js | 19 ++---- .../static/src/core_ui/message_patch.js | 14 +--- .../snailmail_notification_popover.xml | 15 +++-- .../static/src/widgets/popover_widget.js | 13 +--- .../model_field_selector.js | 42 +++++------- .../unique_popover_hook.js | 20 ------ .../static/src/core/popover/popover_hook.js | 62 +++++++++-------- addons/web/static/src/views/calendar/hooks.js | 27 ++++---- .../fields/copy_clipboard/copy_button.js | 16 ++--- .../views/fields/dynamic_placeholder_hook.js | 44 ++++++------ .../many2many_tags/many2many_tags_field.js | 45 +++++-------- .../many2many_tags_avatar_field.js | 37 ++++------ .../many2one_avatar/many2one_avatar_field.js | 36 +++------- .../fields/properties/properties_field.js | 67 +++++++++---------- .../views/fields/properties/property_tags.js | 22 ++---- .../static/src/views/kanban/kanban_header.js | 39 +++++------ .../static/src/views/kanban/kanban_header.xml | 2 +- .../tests/core/popover/popover_hook_tests.js | 6 +- 27 files changed, 268 insertions(+), 477 deletions(-) delete mode 100644 addons/web/static/src/core/model_field_selector/unique_popover_hook.js 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);