Lexical: Added RTL support for UI dropdown menus

They now show in the correct direction and do not overlap.
Added new helper for RTL bounding box handling.
This commit is contained in:
Dan Brown
2026-05-05 16:37:24 +01:00
parent 0eed869735
commit df831a0564
4 changed files with 77 additions and 19 deletions

View File

@@ -27,7 +27,7 @@ export class EditorTableCreator extends EditorUiElement {
}, rowCells));
}
const display = el('div', {class: 'editor-table-creator-display'}, ['0 x 0']);
const display = el('div', {class: 'editor-table-creator-display', dir: 'ltr'}, ['0 x 0']);
const grid = el('div', {class: 'editor-table-creator-grid'}, rows);
grid.addEventListener('mousemove', event => {
const cell = (event.target as HTMLElement).closest('.editor-table-creator-cell') as HTMLElement|null;

View File

@@ -1,3 +1,5 @@
import {getViewportRect, RTLRect} from "../../../utils/rtl";
interface HandleDropdownParams {
toggle: HTMLElement;
menu: HTMLElement;
@@ -7,30 +9,31 @@ interface HandleDropdownParams {
showAside?: boolean;
}
function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) {
const toggleRect = toggle.getBoundingClientRect();
const menuBounds = menu.getBoundingClientRect();
function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean, isRTL: boolean) {
const toggleRect = new RTLRect(toggle, isRTL);
const menuBounds = new RTLRect(menu, isRTL);
const viewport = getViewportRect();
menu.style.position = 'fixed';
if (showAside) {
let targetLeft = toggleRect.right;
const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth;
if (isRightOOB) {
targetLeft = Math.max(toggleRect.left - menuBounds.width, 0);
let targetLeft = toggleRect.inlineEnd;
const isEndOOB = toggleRect.inlineEnd + menuBounds.width > viewport.width;
if (isEndOOB) {
targetLeft = Math.max(toggleRect.inlineStart - menuBounds.width, 0);
}
menu.style.top = toggleRect.top + 'px';
menu.style.left = targetLeft + 'px';
menu.style.top = toggleRect.blockStart + 'px';
menu.style.insetInlineStart = targetLeft + 'px';
} else {
const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth;
let targetLeft = toggleRect.left;
if (isRightOOB) {
targetLeft = Math.max(toggleRect.right - menuBounds.width, 0);
const isEndOOB = toggleRect.inlineStart + menuBounds.width > viewport.width;
let targetLeft = toggleRect.inlineStart;
if (isEndOOB) {
targetLeft = Math.max(toggleRect.inlineEnd - menuBounds.width, 0);
}
menu.style.top = toggleRect.bottom + 'px';
menu.style.left = targetLeft + 'px';
menu.style.top = toggleRect.blockEnd + 'px';
menu.style.insetInlineStart = targetLeft + 'px';
}
}
@@ -38,6 +41,7 @@ export class DropDownManager {
protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();
protected openDropdowns: Set<HTMLElement> = new Set();
protected isRTL: boolean = false;
constructor() {
this.onMenuMouseOver = this.onMenuMouseOver.bind(this);
@@ -46,6 +50,10 @@ export class DropDownManager {
window.addEventListener('click', this.onWindowClick);
}
setIsRTL(isRTL: boolean): void {
this.isRTL = isRTL;
}
teardown(): void {
window.removeEventListener('click', this.onWindowClick);
}
@@ -80,7 +88,7 @@ export class DropDownManager {
protected closeDropdown(menu: HTMLElement): void {
menu.hidden = true;
menu.style.removeProperty('position');
menu.style.removeProperty('left');
menu.style.removeProperty('inset-inline-start');
menu.style.removeProperty('top');
this.openDropdowns.delete(menu);
@@ -94,8 +102,8 @@ export class DropDownManager {
protected openDropdown(menu: HTMLElement): void {
const {toggle, showAside, onOpen} = this.getOptions(menu);
menu.hidden = false
positionMenu(menu, toggle, Boolean(showAside));
menu.hidden = false;
positionMenu(menu, toggle, Boolean(showAside), this.isRTL);
this.openDropdowns.add(menu);
menu.addEventListener('mouseover', this.onMenuMouseOver);

View File

@@ -30,6 +30,7 @@ export class EditorUIManager {
this.context = context;
this.setupEventListeners();
this.setupEditor(context.editor, context);
this.dropdowns.setIsRTL(this.context.manager.getDefaultDirection() === 'rtl');
}
getContext(): EditorUiContext {

View File

@@ -0,0 +1,49 @@
/**
* Create a viewport relative rect for an element which provides
* logical property support.
*/
export class RTLRect {
protected rect: DOMRect;
protected isRTL: boolean;
constructor(element: HTMLElement, isRTL = false) {
this.rect = element.getBoundingClientRect();
this.isRTL = isRTL;
}
get blockStart(): number {
return this.rect.top;
}
get inlineStart(): number {
if (!this.isRTL) {
return this.rect.left;
}
return window.innerWidth - this.rect.right;
}
get blockEnd(): number {
return this.rect.bottom;
}
get inlineEnd(): number {
if (!this.isRTL) {
return this.rect.right;
}
return window.innerWidth - this.rect.left;
}
get width(): number {
return this.rect.width;
}
get height(): number {
return this.rect.height;
}
}
export function getViewportRect(): RTLRect {
return new RTLRect(document.documentElement, false);
}