fix: render popup directly in <body>

This commit is contained in:
JackUait 2025-11-22 12:57:11 +03:00
commit 9b0351f6db
8 changed files with 94 additions and 11 deletions

View file

@ -128,8 +128,9 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
* Open Block Settings pane
*
* @param targetBlock - near which Block we should open BlockSettings
* @param trigger - element to position the popover relative to
*/
public async open(targetBlock?: Block): Promise<void> {
public async open(targetBlock?: Block, trigger?: HTMLElement): Promise<void> {
const block = targetBlock ?? this.Editor.BlockManager.currentBlock;
if (block === undefined) {
@ -159,6 +160,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
const popoverParams: PopoverParams & { flipper?: Flipper } = {
searchable: true,
trigger: trigger || this.nodes.wrapper,
items: await this.getTunesItems(block, commonTunes, toolTunes),
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
messages: {
@ -175,8 +177,6 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
this.popover.on(PopoverEvent.Closed, this.onPopoverClose);
this.nodes.wrapper?.append(this.popover.getElement());
this.popover.show();
if (PopoverClass === PopoverDesktop) {
this.flipperInstance.focusItem(0);

View file

@ -523,6 +523,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
filter: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
},
triggerElement: this.nodes.plusButton,
});
this.toolboxInstance.on(ToolboxEvent.Opened, () => {
@ -671,7 +672,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else {
void this.Editor.BlockSettings.open(hoveredBlock);
void this.Editor.BlockSettings.open(hoveredBlock, this.nodes.settingsToggler);
}
}

View file

@ -123,19 +123,31 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
};
}
/**
* Element relative to which the popover should be positioned
*/
private triggerElement?: HTMLElement;
/**
* Toolbox constructor
*
* @param options - available parameters
* @param options.api - Editor API methods
* @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not
* @param options.triggerElement - Element relative to which the popover should be positioned
*/
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockToolAdapter>; i18nLabels: Record<ToolboxTextLabelsKeys, string>}) {
constructor({ api, tools, i18nLabels, triggerElement }: {
api: API;
tools: ToolsCollection<BlockToolAdapter>;
i18nLabels: Record<ToolboxTextLabelsKeys, string>;
triggerElement?: HTMLElement;
}) {
super();
this.api = api;
this.tools = tools;
this.i18nLabels = i18nLabels;
this.triggerElement = triggerElement;
this.enableShortcuts();
@ -245,6 +257,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
this.popover = new PopoverClass({
scopeElement: this.api.ui.nodes.redactor,
trigger: this.triggerElement || this.nodes.toolbox,
searchable: true,
messages: {
nothingFound: this.i18nLabels.nothingFound,
@ -254,7 +267,6 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
});
this.popover.on(PopoverEvent.Closed, this.onPopoverClose);
this.nodes.toolbox?.append(this.popover.getElement());
}
/**

View file

@ -35,7 +35,8 @@ export class PopoverItemDefault extends PopoverItem {
* Item title
*/
public get title(): string | undefined {
return this.params.title;
// eslint-disable-next-line deprecation/deprecation -- TODO: remove this once label is removed
return this.params.title || this.params.label;
}
/**
@ -173,9 +174,12 @@ export class PopoverItemDefault extends PopoverItem {
el.appendChild(this.nodes.icon);
if (params.title !== undefined) {
// eslint-disable-next-line deprecation/deprecation -- TODO: remove this once label is removed
const title = params.title || params.label;
if (title !== undefined) {
el.appendChild(Dom.make('div', css.title, {
innerHTML: params.title || '',
innerHTML: title || '',
}));
}

View file

@ -116,6 +116,10 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
* Open popover
*/
public show(): void {
if (!this.nodes.popover.isConnected) {
document.body.appendChild(this.nodes.popover);
}
this.nodes.popover.classList.add(css.popoverOpened);
this.nodes.popover.setAttribute('data-popover-opened', 'true');

View file

@ -53,6 +53,11 @@ export class PopoverDesktop extends PopoverAbstract {
*/
private scopeElement: HTMLElement = document.body;
/**
* Element relative to which the popover should be positioned
*/
private trigger: HTMLElement | undefined;
/**
* Construct the instance
*
@ -63,6 +68,10 @@ export class PopoverDesktop extends PopoverAbstract {
constructor(params: PopoverParams, itemsRenderParams?: PopoverItemRenderParamsMap) {
super(params, itemsRenderParams);
if (params.trigger) {
this.trigger = params.trigger;
}
if (params.nestingLevel !== undefined) {
this.nestingLevel = params.nestingLevel;
}
@ -144,13 +153,24 @@ export class PopoverDesktop extends PopoverAbstract {
* Open popover
*/
public show(): void {
if (this.trigger) {
document.body.appendChild(this.nodes.popover);
const { top, left } = this.calculatePosition();
this.nodes.popover.style.position = 'absolute';
this.nodes.popover.style.top = `${top}px`;
this.nodes.popover.style.left = `${left}px`;
this.nodes.popover.style.setProperty('--popover-top', '0px');
this.nodes.popover.style.setProperty('--popover-left', '0px');
}
this.nodes.popover.style.setProperty(CSSVariables.PopoverHeight, this.size.height + 'px');
if (!this.shouldOpenBottom) {
if (!this.trigger && !this.shouldOpenBottom) {
this.nodes.popover.classList.add(css.popoverOpenTop);
}
if (!this.shouldOpenRight) {
if (!this.trigger && !this.shouldOpenRight) {
this.nodes.popover.classList.add(css.popoverOpenLeft);
}
@ -158,6 +178,38 @@ export class PopoverDesktop extends PopoverAbstract {
this.flipper?.activate(this.flippableElements);
}
/**
* Calculates position for the popover
*/
private calculatePosition(): { top: number; left: number } {
if (!this.trigger) {
return {
top: 0,
left: 0,
};
}
const triggerRect = this.trigger.getBoundingClientRect();
const popoverRect = this.size;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const offset = 8;
const initialTop = triggerRect.bottom + offset + window.scrollY;
const shouldFlipTop = (triggerRect.bottom + offset + popoverRect.height > windowHeight + window.scrollY) &&
(triggerRect.top - offset - popoverRect.height > window.scrollY);
const top = shouldFlipTop ? triggerRect.top - offset - popoverRect.height + window.scrollY : initialTop;
const initialLeft = triggerRect.left + window.scrollX;
const shouldFlipLeft = initialLeft + popoverRect.width > windowWidth + window.scrollX;
const left = shouldFlipLeft ? Math.max(0, triggerRect.right - popoverRect.width + window.scrollX) : initialLeft;
return {
top,
left,
};
}
/**
* Closes popover
*/

View file

@ -128,6 +128,11 @@ export interface PopoverItemDefaultBaseParams {
*/
title?: string;
/**
* @deprecated Use title instead
*/
label?: string;
/**
* Item icon to be appeared near a title
*/

View file

@ -17,6 +17,11 @@ export interface PopoverParams {
*/
scopeElement?: HTMLElement;
/**
* Element relative to which the popover should be positioned
*/
trigger?: HTMLElement;
/**
* True if popover should contain search field
*/