Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 24a9e1e

Browse files
committedMar 18, 2025·
[IMP] menu: more positioning options for Menu component
The `Menu` component was taking a `{x,y}` props for its positioning. This is not enough to cover that cases when we want the menu to be positioned around a rectangle when there is not enough space for the menu to be displayed at the bottom left. With this commit: - the menu of `SelectMenu` is does not overlap anymore with the select when the menu is displayed upwards - the menu of `Figure` is positioned right of the figure, and only flip to the left if there is not enough space - the menu of `LinkEditor` is positioned right of the menu button, and not to a random position Task: TODO
1 parent 69366d5 commit 24a9e1e

26 files changed

+118
-94
lines changed
 

‎src/components/bottom_bar/bottom_bar.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
124124
menuState: BottomBarMenuState = useState({
125125
isOpen: false,
126126
menuId: undefined,
127-
position: null,
127+
anchorRect: null,
128128
menuItems: [],
129129
});
130130

@@ -192,7 +192,7 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
192192
this.menuState.isOpen = true;
193193
this.menuState.menuId = menuId;
194194
this.menuState.menuItems = registry.getMenuItems();
195-
this.menuState.position = { x, y };
195+
this.menuState.anchorRect = { x, y, width: 0, height: 0 };
196196
}
197197

198198
onSheetContextMenu(sheetId: UID, registry: MenuItemRegistry, ev: MenuMouseEvent) {
@@ -209,7 +209,7 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
209209
this.menuState.isOpen = false;
210210
this.menuState.menuId = undefined;
211211
this.menuState.menuItems = [];
212-
this.menuState.position = null;
212+
this.menuState.anchorRect = null;
213213
}
214214

215215
closeContextMenuWithId(menuId: UID) {

‎src/components/bottom_bar/bottom_bar.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
<Menu
9090
t-if="menuState.isOpen"
91-
position="menuState.position"
91+
anchorRect="menuState.anchorRect"
9292
menuItems="menuState.menuItems"
9393
maxHeight="menuMaxHeight"
9494
onClose="() => this.closeMenu()"

‎src/components/figures/figure/figure.ts

+6-16
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { Component, onWillUnmount, useEffect, useRef, useState } from "@odoo/owl
22
import {
33
ComponentsImportance,
44
FIGURE_BORDER_COLOR,
5-
MENU_WIDTH,
65
SELECTION_BORDER_COLOR,
76
} from "../../../constants";
87
import { figureRegistry } from "../../../registries/index";
98
import {
109
CSSProperties,
11-
DOMCoordinates,
1210
Figure,
1311
Pixel,
12+
Rect,
1413
ResizeDirection,
1514
SpreadsheetChildEnv,
1615
UID,
@@ -134,7 +133,7 @@ export class FigureComponent extends Component<Props, SpreadsheetChildEnv> {
134133
onClickAnchor: () => {},
135134
};
136135

137-
private menuState: MenuState = useState({ isOpen: false, position: null, menuItems: [] });
136+
private menuState: MenuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
138137

139138
private figureRef = useRef("figure");
140139
private menuButtonRef = useRef("menuButton");
@@ -281,25 +280,16 @@ export class FigureComponent extends Component<Props, SpreadsheetChildEnv> {
281280

282281
onContextMenu(ev: MouseEvent) {
283282
if (this.env.isDashboard()) return;
284-
const position = {
285-
x: ev.clientX,
286-
y: ev.clientY,
287-
};
288-
this.openContextMenu(position);
283+
this.openContextMenu({ x: ev.clientX, y: ev.clientY, width: 0, height: 0 });
289284
}
290285

291286
showMenu() {
292-
const { x, y, width } = this.menuButtonRect;
293-
const menuPosition = {
294-
x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,
295-
y: y,
296-
};
297-
this.openContextMenu(menuPosition);
287+
this.openContextMenu(this.menuButtonRect);
298288
}
299289

300-
private openContextMenu(position: DOMCoordinates) {
290+
private openContextMenu(anchorRect: Rect) {
301291
this.menuState.isOpen = true;
302-
this.menuState.position = position;
292+
this.menuState.anchorRect = anchorRect;
303293
this.menuState.menuItems = figureRegistry
304294
.get(this.props.figure.tag)
305295
.menuBuilder(this.props.figure.id, this.props.onFigureDeleted, this.env);

‎src/components/figures/figure/figure.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
</div>
2929
<Menu
3030
t-if="menuState.isOpen"
31-
position="menuState.position"
31+
anchorRect="menuState.anchorRect"
3232
menuItems="menuState.menuItems"
3333
onClose="() => this.menuState.isOpen=false"
3434
/>

‎src/components/grid/grid.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
153153
this.highlightStore = useStore(HighlightStore);
154154
this.menuState = useState({
155155
isOpen: false,
156-
position: null,
156+
anchorRect: null,
157157
menuItems: [],
158158
});
159159
this.gridRef = useRef("grid");
@@ -593,7 +593,7 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
593593
this.cellPopovers.close();
594594
}
595595
this.menuState.isOpen = true;
596-
this.menuState.position = { x, y };
596+
this.menuState.anchorRect = { x, y, width: 0, height: 0 };
597597
this.menuState.menuItems = registries[type].getMenuItems();
598598
}
599599

‎src/components/grid/grid.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<Menu
5555
t-if="menuState.isOpen"
5656
menuItems="menuState.menuItems"
57-
position="menuState.position"
57+
anchorRect="menuState.anchorRect"
5858
onClose="() => this.closeMenu()"
5959
/>
6060
<t t-foreach="staticTables" t-as="table" t-key="table.id">

‎src/components/header_group/header_group_container.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class HeaderGroupContainer extends Component<Props, SpreadsheetChildEnv>
4444
};
4545
static components = { RowGroup, ColGroup, Menu };
4646

47-
menu: MenuState = useState({ isOpen: false, position: null, menuItems: [] });
47+
menu: MenuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
4848

4949
getLayerOffset(layerIndex: number): number {
5050
return layerIndex * GROUP_LAYER_WIDTH;
@@ -59,13 +59,13 @@ export class HeaderGroupContainer extends Component<Props, SpreadsheetChildEnv>
5959

6060
openContextMenu(position: DOMCoordinates, menuItems: Action[]) {
6161
this.menu.isOpen = true;
62-
this.menu.position = position;
62+
this.menu.anchorRect = { ...position, width: 0, height: 0 };
6363
this.menu.menuItems = menuItems;
6464
}
6565

6666
closeMenu() {
6767
this.menu.isOpen = false;
68-
this.menu.position = null;
68+
this.menu.anchorRect = null;
6969
this.menu.menuItems = [];
7070
}
7171

‎src/components/header_group/header_group_container.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
<Menu
6262
t-if="menu.isOpen"
6363
menuItems="menu.menuItems"
64-
position="menu.position"
64+
anchorRect="menu.anchorRect"
6565
onClose.bind="this.closeMenu"
6666
/>
6767
</div>

‎src/components/link/link_editor/link_editor.ts

+3-12
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import { markdownLink } from "../../../helpers";
33
import { detectLink, urlRepresentation } from "../../../helpers/links";
44
import { canonicalizeNumberContent } from "../../../helpers/locale";
55
import { linkMenuRegistry } from "../../../registries/menus/link_menu_registry";
6-
import { DOMCoordinates, Link, Position, SpreadsheetChildEnv } from "../../../types";
6+
import { Link, Position, SpreadsheetChildEnv } from "../../../types";
77
import { CellPopoverComponent, PopoverBuilders } from "../../../types/cell_popovers";
88
import { css } from "../../helpers/css";
99
import { useAbsoluteBoundingRect } from "../../helpers/position_hook";
1010
import { Menu } from "../../menu/menu";
1111

12-
const MENU_OFFSET_X = 320;
13-
const MENU_OFFSET_Y = 100;
1412
const PADDING = 12;
1513
const LINK_EDITOR_WIDTH = 340 + 2 * PADDING;
1614

@@ -84,8 +82,8 @@ export class LinkEditor extends Component<LinkEditorProps, SpreadsheetChildEnv>
8482
private menu = useState({
8583
isOpen: false,
8684
});
87-
private linkEditorRef = useRef("linkEditor");
88-
private position: DOMCoordinates = useAbsoluteBoundingRect(this.linkEditorRef);
85+
private linkEditorMenuButtonRef = useRef("linkEditorMenuButton");
86+
menuButtonRect = useAbsoluteBoundingRect(this.linkEditorMenuButtonRef);
8987
urlInput = useRef("urlInput");
9088

9189
setup() {
@@ -110,13 +108,6 @@ export class LinkEditor extends Component<LinkEditorProps, SpreadsheetChildEnv>
110108
};
111109
}
112110

113-
get menuPosition(): DOMCoordinates {
114-
return {
115-
x: this.position.x + MENU_OFFSET_X - PADDING - 2,
116-
y: this.position.y + MENU_OFFSET_Y,
117-
};
118-
}
119-
120111
onSpecialLink(ev: CustomEvent<string>) {
121112
const { detail: markdownLink } = ev;
122113
const link = detectLink(markdownLink);

‎src/components/link/link_editor/link_editor.xml

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
<div
44
class="o-link-editor"
55
t-on-click.stop="() => this.menu.isOpen=false"
6-
t-on-keydown="onKeyDown"
7-
t-ref="linkEditor">
6+
t-on-keydown="onKeyDown">
87
<div class="o-section">
98
<div class="o-section-title">Text</div>
109
<div class="d-flex">
@@ -41,14 +40,18 @@
4140
<button t-if="link.url" t-on-click="removeLink" class="o-remove-url o-button-icon">
4241
4342
</button>
44-
<button t-if="!link.url" t-on-click.stop="openMenu" class="o-special-link o-button-icon">
43+
<button
44+
t-if="!link.url"
45+
t-on-click.stop="openMenu"
46+
class="o-special-link o-button-icon"
47+
t-ref="linkEditorMenuButton">
4548
<t t-call="o-spreadsheet-Icon.LIST"/>
4649
</button>
4750
</div>
4851
</div>
4952
<Menu
5053
t-if="menu.isOpen"
51-
position="menuPosition"
54+
anchorRect="menuButtonRect"
5255
menuItems="menuItems"
5356
onMenuClicked="(ev) => this.onSpecialLink(ev)"
5457
onClose="() => this.menu.isOpen=false"

‎src/components/menu/menu.ts

+21-15
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
MENU_VERTICAL_PADDING,
1919
MENU_WIDTH,
2020
} from "../../constants";
21-
import { DOMCoordinates, MenuMouseEvent, Pixel, SpreadsheetChildEnv, UID } from "../../types";
21+
import { DOMCoordinates, MenuMouseEvent, Pixel, Rect, SpreadsheetChildEnv, UID } from "../../types";
22+
import { PopoverPropsPosition } from "../../types/cell_popovers";
2223
import { css, cssPropertiesToCss } from "../helpers/css";
2324
import { getOpenedMenus, isChildEvent, isMiddleClickOrCtrlClick } from "../helpers/dom_helpers";
2425
import { useAbsoluteBoundingRect } from "../helpers/position_hook";
@@ -81,7 +82,8 @@ type MenuItemOrSeparator = Action | "separator";
8182
const TIMEOUT_DELAY = 250;
8283

8384
interface Props {
84-
position: DOMCoordinates;
85+
anchorRect: Rect;
86+
popoverPositioning: PopoverPropsPosition;
8587
menuItems: Action[];
8688
depth: number;
8789
maxHeight?: Pixel;
@@ -95,7 +97,7 @@ interface Props {
9597
export interface MenuState {
9698
isOpen: boolean;
9799
parentMenu?: Action;
98-
position: null | DOMCoordinates;
100+
anchorRect: null | Rect;
99101
scrollOffset?: Pixel;
100102
menuItems: Action[];
101103
isHoveringChild?: boolean;
@@ -104,7 +106,8 @@ export interface MenuState {
104106
export class Menu extends Component<Props, SpreadsheetChildEnv> {
105107
static template = "o-spreadsheet-Menu";
106108
static props = {
107-
position: Object,
109+
anchorRect: Object,
110+
popoverPositioning: { type: String, optional: true },
108111
menuItems: Array,
109112
depth: { type: Number, optional: true },
110113
maxHeight: { type: Number, optional: true },
@@ -118,10 +121,11 @@ export class Menu extends Component<Props, SpreadsheetChildEnv> {
118121
static components = { Menu, Popover };
119122
static defaultProps = {
120123
depth: 1,
124+
popoverPositioning: "TopRight",
121125
};
122126
private subMenu: MenuState = useState({
123127
isOpen: false,
124-
position: null,
128+
anchorRect: null,
125129
scrollOffset: 0,
126130
menuItems: [],
127131
isHoveringChild: false,
@@ -170,22 +174,22 @@ export class Menu extends Component<Props, SpreadsheetChildEnv> {
170174
return menuItemsAndSeparators;
171175
}
172176

173-
get subMenuPosition(): DOMCoordinates {
174-
const position = Object.assign({}, this.subMenu.position);
175-
position.y -= this.subMenu.scrollOffset || 0;
176-
return position;
177+
get subMenuAnchorRect(): Rect {
178+
const anchorRect = Object.assign({}, this.subMenu.anchorRect);
179+
anchorRect.y -= this.subMenu.scrollOffset || 0;
180+
return anchorRect;
177181
}
178182

179183
get popoverProps(): PopoverProps {
180184
const isRoot = this.props.depth === 1;
181185
return {
182186
anchorRect: {
183-
x: this.props.position.x,
184-
y: this.props.position.y,
185-
width: isRoot ? 0 : this.props.width || MENU_WIDTH,
186-
height: isRoot ? 0 : MENU_ITEM_HEIGHT,
187+
x: this.props.anchorRect.x,
188+
y: this.props.anchorRect.y,
189+
width: isRoot ? this.props.anchorRect.width : this.props.width || MENU_WIDTH,
190+
height: isRoot ? this.props.anchorRect.height : MENU_ITEM_HEIGHT,
187191
},
188-
positioning: "TopRight",
192+
positioning: this.props.popoverPositioning,
189193
verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
190194
onPopoverHidden: () => this.closeSubMenu(),
191195
onPopoverMoved: () => this.closeSubMenu(),
@@ -269,9 +273,11 @@ export class Menu extends Component<Props, SpreadsheetChildEnv> {
269273
}
270274
const y = parentMenuEl.getBoundingClientRect().top;
271275

272-
this.subMenu.position = {
276+
this.subMenu.anchorRect = {
273277
x: this.position.x,
274278
y: y - (this.subMenu.scrollOffset || 0),
279+
width: this.props.width || MENU_WIDTH,
280+
height: MENU_ITEM_HEIGHT,
275281
};
276282
this.subMenu.menuItems = menu.children(this.env);
277283
this.subMenu.isOpen = true;

‎src/components/menu/menu.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
<Menu
6161
t-if="subMenu.isOpen"
6262
t-key="subMenu.parentMenu.id"
63-
position="subMenuPosition"
63+
anchorRect="subMenuAnchorRect"
6464
menuItems="subMenu.menuItems"
6565
depth="props.depth + 1"
6666
maxHeight="props.maxHeight"

‎src/components/side_panel/components/cog_wheel_menu/cog_wheel_menu.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, useRef, useState } from "@odoo/owl";
22
import { ActionSpec, createActions } from "../../../../actions/action";
33
import { MenuMouseEvent } from "../../../../types";
44
import { SpreadsheetChildEnv } from "../../../../types/env";
5+
import { getBoundingRectAsPOJO } from "../../../helpers/dom_helpers";
56
import { Menu, MenuState } from "../../../menu/menu";
67

78
interface Props {
@@ -16,7 +17,7 @@ export class CogWheelMenu extends Component<Props, SpreadsheetChildEnv> {
1617
};
1718

1819
private buttonRef = useRef("button");
19-
private menuState: MenuState = useState({ isOpen: false, position: null, menuItems: [] });
20+
private menuState: MenuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
2021

2122
private menuId = this.env.model.uuidGenerator.uuidv4();
2223

@@ -25,9 +26,8 @@ export class CogWheelMenu extends Component<Props, SpreadsheetChildEnv> {
2526
return;
2627
}
2728

28-
const { x, y } = this.buttonRef.el!.getBoundingClientRect();
2929
this.menuState.isOpen = !this.menuState.isOpen;
30-
this.menuState.position = { x, y };
30+
this.menuState.anchorRect = getBoundingRectAsPOJO(this.buttonRef.el!);
3131
this.menuState.menuItems = createActions(this.props.items);
3232
}
3333
}

‎src/components/side_panel/components/cog_wheel_menu/cog_wheel_menu.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Menu
99
t-if="menuState.isOpen"
1010
menuId="menuId"
11-
position="menuState.position"
11+
anchorRect="menuState.anchorRect"
1212
menuItems="menuState.menuItems"
1313
onClose="() => this.menuState.isOpen=false"
1414
width="160"

‎src/components/side_panel/select_menu/select_menu.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, useRef, useState } from "@odoo/owl";
22
import { Action } from "../../../actions/action";
33
import { UuidGenerator } from "../../../helpers";
4-
import { DOMCoordinates, MenuMouseEvent, SpreadsheetChildEnv } from "../../../types";
4+
import { MenuMouseEvent, Rect, SpreadsheetChildEnv } from "../../../types";
55
import { useAbsoluteBoundingRect } from "../../helpers/position_hook";
66
import { Menu } from "../../menu/menu";
77

@@ -45,10 +45,7 @@ export class SelectMenu extends Component<SelectMenuProps, SpreadsheetChildEnv>
4545
this.state.isMenuOpen = false;
4646
}
4747

48-
get menuPosition(): DOMCoordinates {
49-
return {
50-
x: this.selectRect.x,
51-
y: this.selectRect.y + this.selectRect.height,
52-
};
48+
get menuAnchorRect(): Rect {
49+
return this.selectRect;
5350
}
5451
}

0 commit comments

Comments
 (0)
Please sign in to comment.