Skip to content

Commit 6896610

Browse files
committed
[IMP] Menu: Delegate enabled to children items
Currently, if a menu is evaluated as disabled, we disable it and prevent the possibility to see its children by hover. However, there are some issues with this: - the user is mislead because we keep showing the arrow that suggest the presence of children items - the user can simply not navigate through the submenus to see what kind of feature could be available - if a menu has several children some of which are readonlyAllowed, then the readonlyAllowed condition has to be set on the parent as well; the 'enabling' condition therefore becomes a combination set on both the parent and children - if a parent menu is computed as 'enabled' it will not be greyed out (suggests that it has useful children) even if the said children are not enabled. This revision changes the computation of 'enabled' state of item by looking at the full tree of children and applies the following logic; A parent menu is disabled if every leaf of the tree is disabled. A single 'enabled' leaf (that is a child without children of its own) will make all its parent enabled as well, up to the root. We also allow all submenus to be opened regardless of the state of the parent. The user will therefore have a visual hint that there are active submenus or not (if all children are disabled, so will be the parent) and they will also have the possibility to have a look at all children to explore the possibilities. Task: 5331717
1 parent 92d1026 commit 6896610

File tree

8 files changed

+101
-64
lines changed

8 files changed

+101
-64
lines changed

src/actions/view_actions.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,21 +131,18 @@ export const unFreezeRows: ActionSpec = {
131131
env.model.dispatch("UNFREEZE_ROWS", {
132132
sheetId: env.model.getters.getActiveSheetId(),
133133
}),
134-
isReadonlyAllowed: true,
135134
isVisible: (env) =>
136135
!!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).ySplit,
137136
};
138137

139138
export const freezeFirstRow: ActionSpec = {
140139
name: _t("1 row"),
141140
execute: (env) => interactiveFreezeColumnsRows(env, "ROW", 1),
142-
isReadonlyAllowed: true,
143141
};
144142

145143
export const freezeSecondRow: ActionSpec = {
146144
name: _t("2 rows"),
147145
execute: (env) => interactiveFreezeColumnsRows(env, "ROW", 2),
148-
isReadonlyAllowed: true,
149146
};
150147

151148
export const freezeCurrentRow: ActionSpec = {
@@ -154,7 +151,6 @@ export const freezeCurrentRow: ActionSpec = {
154151
const { bottom } = env.model.getters.getSelectedZone();
155152
interactiveFreezeColumnsRows(env, "ROW", bottom + 1);
156153
},
157-
isReadonlyAllowed: true,
158154
};
159155

160156
export const unFreezeCols: ActionSpec = {
@@ -163,21 +159,18 @@ export const unFreezeCols: ActionSpec = {
163159
env.model.dispatch("UNFREEZE_COLUMNS", {
164160
sheetId: env.model.getters.getActiveSheetId(),
165161
}),
166-
isReadonlyAllowed: true,
167162
isVisible: (env) =>
168163
!!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).xSplit,
169164
};
170165

171166
export const freezeFirstCol: ActionSpec = {
172167
name: _t("1 column"),
173168
execute: (env) => interactiveFreezeColumnsRows(env, "COL", 1),
174-
isReadonlyAllowed: true,
175169
};
176170

177171
export const freezeSecondCol: ActionSpec = {
178172
name: _t("2 columns"),
179173
execute: (env) => interactiveFreezeColumnsRows(env, "COL", 2),
180-
isReadonlyAllowed: true,
181174
};
182175

183176
export const freezeCurrentCol: ActionSpec = {
@@ -186,7 +179,6 @@ export const freezeCurrentCol: ActionSpec = {
186179
const { right } = env.model.getters.getSelectedZone();
187180
interactiveFreezeColumnsRows(env, "COL", right + 1);
188181
},
189-
isReadonlyAllowed: true,
190182
};
191183

192184
export const viewGridlines: ActionSpec = {

src/components/menu/menu.css

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,26 @@
1212
var(--os-desktop-menu-item-height) - 2 * var(--os-menu-item-padding-vertical)
1313
);
1414
}
15-
&:not(.disabled) {
15+
& {
1616
&:hover,
1717
&.o-menu-item-active {
1818
background-color: var(--os-button-active-bg);
1919
color: var(--os-button-active-text-color);
2020
}
21+
.o-menu-item-icon {
22+
.o-icon {
23+
color: var(--os-icons-color);
24+
}
25+
}
26+
.o-menu-item-description {
27+
color: grey;
28+
}
2129
}
2230
&.disabled {
23-
color: var(--os-disabled-text-color);
31+
&,
32+
.o-menu-item-description {
33+
color: var(--os-disabled-text-color);
34+
}
2435
}
2536
.o-menu-item-name {
2637
min-width: 40%;
@@ -29,12 +40,6 @@
2940
display: inline-block;
3041
margin: 0px 8px 0px 0px;
3142
}
32-
.o-menu-item-description {
33-
color: grey;
34-
}
35-
&.disabled {
36-
cursor: not-allowed;
37-
}
3843
}
3944
}
4045
&.o-spreadsheet-mobile {

src/components/menu/menu.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,15 @@ export class Menu extends Component<MenuProps, SpreadsheetChildEnv> {
118118
}
119119

120120
isEnabled(menu: Action) {
121-
if (menu.isEnabled(this.env)) {
122-
return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
121+
const children = menu.children?.(this.env);
122+
if (children.length) {
123+
return children.some((child) => this.isEnabled(child));
124+
} else {
125+
if (menu.isEnabled(this.env)) {
126+
return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
127+
}
128+
return false;
123129
}
124-
return false;
125130
}
126131

127132
get menuStyle() {

src/components/menu_popover/menu_popover.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,15 @@ export class MenuPopover extends Component<Props, SpreadsheetChildEnv> {
245245
}
246246

247247
onMouseOver(menu: Action, ev: MouseEvent) {
248-
if (this.isEnabled(menu)) {
249-
if (this.isParentMenu(this.subMenu, menu)) {
250-
this.openingTimeOut.clear();
251-
return;
252-
}
253-
const currentTarget = ev.currentTarget as HTMLElement;
254-
if (this.isRoot(menu)) {
255-
this.openingTimeOut.schedule(() => {
256-
this.openSubMenu(menu, currentTarget);
257-
}, TIMEOUT_DELAY);
258-
}
248+
if (this.isParentMenu(this.subMenu, menu)) {
249+
this.openingTimeOut.clear();
250+
return;
251+
}
252+
const currentTarget = ev.currentTarget as HTMLElement;
253+
if (this.isRoot(menu)) {
254+
this.openingTimeOut.schedule(() => {
255+
this.openSubMenu(menu, currentTarget);
256+
}, TIMEOUT_DELAY);
259257
}
260258
}
261259

src/registries/menus/number_format_menu_registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Registry } from "@odoo/o-spreadsheet-engine/registries/registry";
22
import { _t } from "@odoo/o-spreadsheet-engine/translation";
33
import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env";
4-
import { ActionSpec, createActions } from "../../actions/action";
4+
import { ActionSpec } from "../../actions/action";
55
import * as ACTION_FORMAT from "../../actions/format_actions";
66
import { isDateTimeFormat, memoize } from "../../helpers";
77
import { Format } from "../../types";
@@ -138,7 +138,7 @@ export const formatNumberMenuItemSpec: ActionSpec = {
138138
if (customFormats.length > 0) {
139139
customFormats[customFormats.length - 1].separator = true;
140140
}
141-
return createActions([...numberFormatMenuRegistry.getAll(), ...customFormats]);
141+
return [...numberFormatMenuRegistry.getAll(), ...customFormats];
142142
},
143143
],
144144
};

src/registries/menus/topbar_menu_registry.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ topbarMenuRegistry
2222
.add("file", {
2323
name: _t("File"),
2424
sequence: 10,
25-
isReadonlyAllowed: true,
2625
})
2726
.addChild("settings", ["file"], {
2827
name: _t("Settings"),
@@ -39,7 +38,6 @@ topbarMenuRegistry
3938
.add("edit", {
4039
name: _t("Edit"),
4140
sequence: 20,
42-
isReadonlyAllowed: true,
4341
})
4442
.addChild("undo", ["edit"], {
4543
...ACTION_EDIT.undo,
@@ -126,7 +124,6 @@ topbarMenuRegistry
126124
.add("view", {
127125
name: _t("View"),
128126
sequence: 30,
129-
isReadonlyAllowed: true,
130127
})
131128
.addChild("unfreeze_panes", ["view"], {
132129
...ACTION_VIEW.unFreezePane,
@@ -225,7 +222,6 @@ topbarMenuRegistry
225222
.add("insert", {
226223
name: _t("Insert"),
227224
sequence: 40,
228-
isReadonlyAllowed: true,
229225
})
230226
.addChild("insert_row", ["insert"], {
231227
...ACTION_INSERT.insertRow,
@@ -346,7 +342,6 @@ topbarMenuRegistry
346342
.add("format", {
347343
name: _t("Format"),
348344
sequence: 50,
349-
isReadonlyAllowed: true,
350345
})
351346
.addChild("format_number", ["format"], {
352347
...formatNumberMenuItemSpec,
@@ -441,7 +436,6 @@ topbarMenuRegistry
441436
.add("data", {
442437
name: _t("Data"),
443438
sequence: 60,
444-
isReadonlyAllowed: true,
445439
})
446440
.addChild("sort_range", ["data"], {
447441
...ACTION_DATA.sortRange,

tests/menus/context_menu_component.test.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -449,33 +449,51 @@ describe("Context MenuPopover internal tests", () => {
449449
expect(menuItem2?.classList).toContain("o-menu-item-active");
450450
});
451451

452-
test("submenu does not open when disabled", async () => {
453-
const menuItems: Action[] = createActions([
454-
makeTestMenuItem("root", {
455-
isEnabled: () => false,
456-
children: [makeTestMenuItem("subMenu")],
457-
}),
458-
]);
459-
await renderContextMenu(300, 300, { menuItems });
460-
expect(fixture.querySelector(".o-menu div[data-name='root']")!.classList).toContain("disabled");
461-
await simulateClick(".o-menu div[data-name='root']");
462-
expect(fixture.querySelector(".o-menu div[data-name='subMenu']")).toBeFalsy();
463-
});
452+
describe("IsEnabled is ignored on parent menu items", () => {
453+
test("submenu opens even if the parent is disabled", async () => {
454+
const menuItems: Action[] = createActions([
455+
makeTestMenuItem("root", {
456+
isEnabled: () => false,
457+
children: [makeTestMenuItem("subMenu")],
458+
}),
459+
]);
460+
await renderContextMenu(300, 300, { menuItems });
461+
expect(fixture.querySelector(".o-menu div[data-name='root']")!).not.toHaveClass("disabled");
462+
await simulateClick(".o-menu div[data-name='root']");
463+
expect(fixture.querySelector(".o-menu div[data-name='subMenu']")).toBeTruthy();
464+
});
464465

465-
test("submenu does not open when hovering write only parent", async () => {
466-
const menuItems: Action[] = createActions([
467-
makeTestMenuItem("root", {
468-
isReadonlyAllowed: false,
469-
isEnabled: () => true,
470-
children: [makeTestMenuItem("subMenu")],
471-
}),
472-
]);
473-
await renderContextMenu(300, 300, { menuItems });
474-
model.updateMode("readonly");
475-
await nextTick();
476-
expect(fixture.querySelector(".o-menu div[data-name='root']")!.classList).toContain("disabled");
477-
await mouseOverMenuElement(".o-menu div[data-name='root']");
478-
expect(fixture.querySelector(".o-menu div[data-name='subMenu']")).toBeFalsy();
466+
test("in readonly, submenu opens if the children are readonly and the parent write only", async () => {
467+
const menuItems: Action[] = createActions([
468+
makeTestMenuItem("root", {
469+
isReadonlyAllowed: false,
470+
isEnabled: () => false,
471+
children: [makeTestMenuItem("subMenu", { isReadonlyAllowed: true })],
472+
}),
473+
]);
474+
await renderContextMenu(300, 300, { menuItems });
475+
model.updateMode("readonly");
476+
await nextTick();
477+
expect(fixture.querySelector(".o-menu div[data-name='root']")!).not.toHaveClass("disabled");
478+
await mouseOverMenuElement(".o-menu div[data-name='root']");
479+
expect(fixture.querySelector(".o-menu div[data-name='subMenu']")).toBeTruthy();
480+
});
481+
482+
test("in readonly, submenu opens if the children are readonly and the parent write only", async () => {
483+
const menuItems: Action[] = createActions([
484+
makeTestMenuItem("root", {
485+
isReadonlyAllowed: false,
486+
isEnabled: () => true,
487+
children: [makeTestMenuItem("subMenu")],
488+
}),
489+
]);
490+
await renderContextMenu(300, 300, { menuItems });
491+
model.updateMode("readonly");
492+
await nextTick();
493+
expect(fixture.querySelector(".o-menu div[data-name='root']")!).toHaveClass("disabled");
494+
await mouseOverMenuElement(".o-menu div[data-name='root']");
495+
expect(fixture.querySelector(".o-menu div[data-name='subMenu']")).toBeTruthy();
496+
});
479497
});
480498

481499
test("submenu does not close when sub item hovered", async () => {

tests/menus/menu_component.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Model } from "@odoo/o-spreadsheet-engine";
12
import { createActions } from "../../src/actions/action";
23
import { Menu } from "../../src/components/menu/menu";
34
import { simulateClick } from "../test_helpers/dom_helper";
@@ -24,4 +25,28 @@ describe("Menu component", () => {
2425
await simulateClick(selector);
2526
expect(callback).not.toHaveBeenCalled();
2627
});
28+
29+
test("Execute is not called when menu item is ont readonly allowed", async () => {
30+
const callback = jest.fn();
31+
const menuItems = createActions([
32+
{
33+
id: "test_menu",
34+
name: "Test Menu",
35+
isEnabled: () => true,
36+
execute: () => {},
37+
},
38+
]);
39+
40+
const selector = ".o-menu div[data-name='test_menu']";
41+
const model = new Model();
42+
model.updateMode("readonly");
43+
const { fixture } = await mountComponent(Menu, {
44+
props: { menuItems, onClose: () => {}, onClickMenu: () => callback() },
45+
env: { model },
46+
});
47+
48+
expect(fixture.querySelector(selector)!.classList).toContain("disabled");
49+
await simulateClick(selector);
50+
expect(callback).not.toHaveBeenCalled();
51+
});
2752
});

0 commit comments

Comments
 (0)