Skip to content

Commit f7fea29

Browse files
authored
feat(ui5-menu-separator): add new component (#8871)
New component that can be slotted with menu items intended to separate the items with a line. BREAKING CHANGE: `startsSection` property removed from MenuItems Before: <ui5-menu> <ui5-menu-item text="Item A"></ui5-menu-item> <ui5-menu-item text="Item B" starts-section></ui5-menu-item> </ui5-menu> Now: <ui5-menu> <ui5-menu-item text="Item A"></ui5-menu-item> <ui5-menu-separator></ui5-menu-separator> <ui5-menu-item text="Item B"></ui5-menu-item> </ui5-menu> Related to: #8461
1 parent ca0509a commit f7fea29

File tree

12 files changed

+163
-40
lines changed

12 files changed

+163
-40
lines changed

packages/main/src/Menu.ts

+35-12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import List from "./List.js";
2626
import Icon from "./Icon.js";
2727
import BusyIndicator from "./BusyIndicator.js";
2828
import MenuItem from "./MenuItem.js";
29+
import MenuSeparator from "./MenuSeparator.js";
2930
import type {
3031
ListItemClickEventDetail,
3132
} from "./List.js";
@@ -39,6 +40,16 @@ import menuCss from "./generated/themes/Menu.css.js";
3940

4041
const MENU_OPEN_DELAY = 300;
4142

43+
/**
44+
* Interface for components that may be slotted inside a `ui5-menu`.
45+
*
46+
* **Note:** Use with `ui5-menu-item` or `ui5-menu-separator`. Implementing the interface does not guarantee that any other classes can work with the `ui5-menu`.
47+
* @public
48+
*/
49+
interface IMenuItem extends UI5Element {
50+
isSeparator: boolean;
51+
}
52+
4253
type MenuItemClickEventDetail = {
4354
item: MenuItem,
4455
text: string,
@@ -54,9 +65,13 @@ type MenuBeforeCloseEventDetail = { escPressed: boolean };
5465
*
5566
* `ui5-menu` component represents a hierarchical menu structure.
5667
*
57-
* ### Usage
68+
* ### Structure
69+
*
70+
* The `ui5-menu` can hold two types of entities:
71+
*
72+
* - `ui5-menu-item` components
73+
* - `ui5-menu-separator` - used to separate menu items with a line
5874
*
59-
* `ui5-menu` contains `ui5-menu-item` components.
6075
* An arbitrary hierarchy structure can be represented by recursively nesting menu items.
6176
*
6277
* ### Keyboard Handling
@@ -89,6 +104,7 @@ type MenuBeforeCloseEventDetail = { escPressed: boolean };
89104
Button,
90105
List,
91106
MenuItem,
107+
MenuSeparator,
92108
Icon,
93109
BusyIndicator,
94110
],
@@ -223,11 +239,11 @@ class Menu extends UI5Element {
223239
/**
224240
* Defines the items of this component.
225241
*
226-
* **Note:** Use `ui5-menu-item` for the intended design.
242+
* **Note:** Use `ui5-menu-item` and `ui5-menu-separator` for their intended design.
227243
* @public
228244
*/
229245
@slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true })
230-
items!: Array<MenuItem>;
246+
items!: Array<IMenuItem>;
231247

232248
static i18nBundle: I18nBundle;
233249
_timeout?: Timeout;
@@ -252,10 +268,14 @@ class Menu extends UI5Element {
252268
return this.shadowRoot!.querySelector<ResponsivePopover>("[ui5-responsive-popover]")!;
253269
}
254270

271+
get _menuItems() {
272+
return this.items.filter((item): item is MenuItem => !item.isSeparator);
273+
}
274+
255275
onBeforeRendering() {
256-
const siblingsWithIcon = this.items.some(item => !!item.icon);
276+
const siblingsWithIcon = this._menuItems.some(menuItem => !!menuItem.icon);
257277

258-
this.items.forEach(item => {
278+
this._menuItems.forEach(item => {
259279
item._siblingsWithIcon = siblingsWithIcon;
260280
});
261281
}
@@ -281,7 +301,7 @@ class Menu extends UI5Element {
281301

282302
_closeItemSubMenu(item: MenuItem) {
283303
if (item && item._popover) {
284-
const openedSibling = item.items.find(menuItem => menuItem._popover && menuItem._popover.open);
304+
const openedSibling = item._menuItems.find(menuItem => menuItem._popover && menuItem._popover.open);
285305
if (openedSibling) {
286306
this._closeItemSubMenu(openedSibling);
287307
}
@@ -296,10 +316,12 @@ class Menu extends UI5Element {
296316
// respect mouseover only on desktop
297317
const item = e.target as MenuItem;
298318

299-
item.focus();
319+
if (item.hasAttribute("ui5-menu-item")) {
320+
item.focus();
300321

301-
// Opens submenu with 300ms delay
302-
this._startOpenTimeout(item);
322+
// Opens submenu with 300ms delay
323+
this._startOpenTimeout(item);
324+
}
303325
}
304326
}
305327

@@ -308,7 +330,7 @@ class Menu extends UI5Element {
308330

309331
this._timeout = setTimeout(() => {
310332
const opener = item.parentElement as MenuItem | Menu;
311-
const openedSibling = opener && opener.items.find(menuItem => menuItem._popover && menuItem._popover.open);
333+
const openedSibling = opener && opener._menuItems.find(menuItem => menuItem._popover && menuItem._popover.open);
312334
if (openedSibling) {
313335
this._closeItemSubMenu(openedSibling);
314336
}
@@ -367,7 +389,7 @@ class Menu extends UI5Element {
367389

368390
_afterPopoverOpen() {
369391
this.open = true;
370-
this.items[0]?.focus();
392+
this._menuItems[0]?.focus();
371393
this.fireEvent("open", {}, false, true);
372394
}
373395

@@ -393,4 +415,5 @@ export type {
393415
MenuItemClickEventDetail,
394416
MenuBeforeCloseEventDetail,
395417
MenuBeforeOpenEventDetail,
418+
IMenuItem,
396419
};

packages/main/src/MenuItem.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MENU_CLOSE_BUTTON_ARIA_LABEL,
1818
} from "./generated/i18n/i18n-defaults.js";
1919
import type { ResponsivePopoverBeforeCloseEventDetail } from "./ResponsivePopover.js";
20+
import type { IMenuItem } from "./Menu.js";
2021

2122
// Styles
2223
import menuItemCss from "./generated/themes/MenuItem.css.js";
@@ -43,6 +44,7 @@ type MenuBeforeCloseEventDetail = { escPressed: boolean };
4344
* `import "@ui5/webcomponents/dist/MenuItem.js";`
4445
* @constructor
4546
* @extends ListItem
47+
* @implements {IMenuItem}
4648
* @since 1.3.0
4749
* @public
4850
*/
@@ -52,7 +54,7 @@ type MenuBeforeCloseEventDetail = { escPressed: boolean };
5254
styles: [ListItem.styles, menuItemCss],
5355
dependencies: [...ListItem.dependencies, ResponsivePopover, List, BusyIndicator, Icon],
5456
})
55-
class MenuItem extends ListItem {
57+
class MenuItem extends ListItem implements IMenuItem {
5658
static async onDefine() {
5759
MenuItem.i18nBundle = await getI18nBundle("@ui5/webcomponents");
5860
}
@@ -93,14 +95,6 @@ class MenuItem extends ListItem {
9395
@property()
9496
icon!: string;
9597

96-
/**
97-
* Defines whether a visual separator should be rendered before the item.
98-
* @default false
99-
* @public
100-
*/
101-
@property({ type: Boolean })
102-
startsSection!: boolean;
103-
10498
/**
10599
* Defines whether `ui5-menu-item` is in disabled state.
106100
*
@@ -158,7 +152,9 @@ class MenuItem extends ListItem {
158152
/**
159153
* Defines the items of this component.
160154
*
161-
* **Note:** If there are items added to this slot, an arrow will be displayed at the end
155+
* **Note:** The slot can hold `ui5-menu-item` and `ui5-menu-separator` items.
156+
*
157+
* If there are items added to this slot, an arrow will be displayed at the end
162158
* of the item in order to indicate that there are items added. In that case components added
163159
* to `endContent` slot or `additionalText` content will not be displayed.
164160
*
@@ -167,7 +163,7 @@ class MenuItem extends ListItem {
167163
* @public
168164
*/
169165
@slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true })
170-
items!: Array<MenuItem>;
166+
items!: Array<IMenuItem>;
171167

172168
/**
173169
* Defines the components that should be displayed at the end of the menu item.
@@ -229,10 +225,14 @@ class MenuItem extends ListItem {
229225
return MenuItem.i18nBundle.getText(MENU_CLOSE_BUTTON_ARIA_LABEL);
230226
}
231227

228+
get isSeparator(): boolean {
229+
return false;
230+
}
231+
232232
onBeforeRendering() {
233-
const siblingsWithIcon = this.items.some(item => !!item.icon);
233+
const siblingsWithIcon = this._menuItems.some(menuItem => !!menuItem.icon);
234234

235-
this.items.forEach(item => {
235+
this._menuItems.forEach(item => {
236236
item._siblingsWithIcon = siblingsWithIcon;
237237
});
238238
}
@@ -254,6 +254,10 @@ class MenuItem extends ListItem {
254254
return this.shadowRoot!.querySelector<ResponsivePopover>("[ui5-responsive-popover]")!;
255255
}
256256

257+
get _menuItems() {
258+
return this.items.filter((item): item is MenuItem => !item.isSeparator);
259+
}
260+
257261
_closeAll() {
258262
if (this._popover) {
259263
this._popover.open = false;

packages/main/src/MenuSeparator.hbs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<ui5-li-custom
2+
class="{{classes.main}}"
3+
role="separator"
4+
disabled
5+
>
6+
</ui5-li-custom>

packages/main/src/MenuSeparator.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
2+
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
3+
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
4+
import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js";
5+
import menuSeparatorTemplate from "./generated/templates/MenuSeparatorTemplate.lit.js";
6+
import menuSeparatorCss from "./generated/themes/MenuSeparator.css.js";
7+
import ListItemBase from "./ListItemBase.js";
8+
import ListItemCustom from "./ListItemCustom.js";
9+
import type { IMenuItem } from "./Menu.js";
10+
/**
11+
* @class
12+
* The `ui5-menu-separator` represents a horizontal line to separate menu items inside a `ui5-menu`.
13+
* @constructor
14+
* @extends ListItemBase
15+
* @implements {IMenuItem}
16+
* @public
17+
* @since 2.0
18+
*/
19+
@customElement({
20+
tag: "ui5-menu-separator",
21+
renderer: litRender,
22+
styles: [menuSeparatorCss],
23+
template: menuSeparatorTemplate,
24+
dependencies: [
25+
ListItemCustom,
26+
],
27+
})
28+
29+
class MenuSeparator extends ListItemBase implements IMenuItem {
30+
/**
31+
* **Note:** This property is inherited and not supported. If set, it won't take any effect.
32+
* @default false
33+
* @public
34+
*/
35+
@property({ type: Boolean })
36+
declare selected: boolean;
37+
38+
get isSeparator() {
39+
return true;
40+
}
41+
42+
get classes(): ClassMap {
43+
return {
44+
main: {
45+
"ui5-menu-separator": true,
46+
},
47+
};
48+
}
49+
50+
/**
51+
* @override
52+
*/
53+
get _focusable() {
54+
return false;
55+
}
56+
57+
/**
58+
* @override
59+
*/
60+
get _pressable() {
61+
return false;
62+
}
63+
}
64+
MenuSeparator.define();
65+
66+
export default MenuSeparator;

packages/main/src/bundle.esm.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import Menu from "./Menu.js";
6363
import NavigationMenu from "./NavigationMenu.js";
6464
import NavigationMenuItem from "./NavigationMenuItem.js";
6565
import MenuItem from "./MenuItem.js";
66+
import MenuSeparator from "./MenuSeparator.js";
6667
import Popover from "./Popover.js";
6768
import Panel from "./Panel.js";
6869
import RadioButton from "./RadioButton.js";

packages/main/src/themes/MenuItem.css

-4
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,6 @@
7777
padding: var(--_ui5_menu_item_padding);
7878
}
7979

80-
:host([starts-section]) {
81-
border-top: 1px solid var(--sapGroup_ContentBorderColor);
82-
}
83-
8480
:host::part(content) {
8581
padding-inline-end: 0.25rem;
8682
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:host {
2+
border-top: 0.0625rem solid var(--sapGroup_ContentBorderColor);
3+
min-height: 0.125rem;
4+
}
5+
6+
.ui5-menu-separator {
7+
border: inherit;
8+
min-height: inherit;
9+
background: inherit;
10+
opacity: 1;
11+
}

packages/main/test/pages/Menu.html

+9-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
<ui5-menu id="menu" header-text="My ui5-menu">
1818
<ui5-menu-item text="New File(selection prevented)" accessible-name="Opens a file explorer" additional-text="Ctrl+Alt+Shift+N" tooltip="Select a file - prevent default" icon="add-document"></ui5-menu-item>
1919
<ui5-menu-item text="New Folder with very long title for a menu item" additional-text="Ctrl+F" icon="add-folder" disabled></ui5-menu-item>
20-
<ui5-menu-item text="Open" icon="open-folder" accessible-name="Choose platform" starts-section loading-delay="100" loading>
20+
<ui5-menu-separator></ui5-menu-separator>
21+
<ui5-menu-item text="Open" icon="open-folder" accessible-name="Choose platform" loading-delay="100" loading>
2122
<ui5-menu-item text="Open Locally" icon="open-folder" additional-text="Ctrl+K">
2223
<ui5-menu-item text="Open from C"></ui5-menu-item>
2324
<ui5-menu-item text="Open from D"></ui5-menu-item>
25+
<ui5-menu-separator></ui5-menu-separator>
2426
<ui5-menu-item text="Open from E" disabled></ui5-menu-item>
2527
</ui5-menu-item>
2628
<ui5-menu-item text="Open from SAP Cloud" additional-text="Ctrl+L"></ui5-menu-item>
@@ -34,7 +36,8 @@
3436
<ui5-menu-item text="Save on Cloud" icon="upload-to-cloud"></ui5-menu-item>
3537
</ui5-menu-item>
3638
<ui5-menu-item text="Close" additional-text="Ctrl+W" loading-delay="100" loading></ui5-menu-item>
37-
<ui5-menu-item text="Preferences" icon="action-settings" starts-section disabled></ui5-menu-item>
39+
<ui5-menu-separator></ui5-menu-separator>
40+
<ui5-menu-item text="Preferences" icon="action-settings" disabled></ui5-menu-item>
3841
<ui5-menu-item text="Exit" icon="journey-arrive"></ui5-menu-item>
3942
</ui5-menu>
4043

@@ -63,11 +66,13 @@
6366
<ui5-button id="btnOpen2">Open Menu</ui5-button> <br/><br/>
6467
<ui5-menu id="menu2" header-text="My ui5-menu">
6568
<ui5-menu-item id="newFolder" text="New Folder" additional-text="Ctrl+F" icon="add-folder" disabled></ui5-menu-item>
66-
<ui5-menu-item text="Open" icon="open-folder" starts-section>
69+
<ui5-menu-separator></ui5-menu-separator>
70+
<ui5-menu-item text="Open" icon="open-folder">
6771
<ui5-menu-item text="Open Locally" icon="open-folder" additional-text="Ctrl+K"></ui5-menu-item>
6872
<ui5-menu-item text="Open from SAP Cloud" additional-text="Ctrl+L" disabled></ui5-menu-item>
6973
</ui5-menu-item>
70-
<ui5-menu-item id="preferences" text="Preferences" icon="action-settings" starts-section disabled></ui5-menu-item>
74+
<ui5-menu-separator></ui5-menu-separator>
75+
<ui5-menu-item id="preferences" text="Preferences" icon="action-settings" disabled></ui5-menu-item>
7176
<ui5-menu-item text="Exit" icon="journey-arrive"></ui5-menu-item>
7277
</ui5-menu>
7378

packages/main/test/specs/Menu.spec.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,17 @@ describe("Menu interaction", () => {
3131
it("Top level menu items appearance", async () => {
3232
await browser.url(`test/pages/Menu.html`);
3333
const openButton = await browser.$("#btnOpen");
34-
const menuItems = await browser.$$("#menu > ui5-menu-item");
34+
const menuItems = await browser.$$("#menu > *");
3535

3636
await openButton.click();
3737

38-
assert.strictEqual(await menuItems.length, 7, "There are proper count of menu items in the top level menu");
38+
assert.strictEqual(await menuItems.length, 9, "There are proper count of menu items in the top level menu");
39+
assert.strictEqual(await menuItems[0].getAttribute("ui5-menu-item"), "", "The first list item is a menu item");
3940
assert.strictEqual(await menuItems[0].getAttribute("additional-text"), "Ctrl+Alt+Shift+N", "The first list item has proper additional text set");
4041
assert.strictEqual(await menuItems[1].getAttribute("disabled"), "true", "The second list item is disabled");
41-
assert.strictEqual(await menuItems[2].getAttribute("starts-section"), "", "The third list item has separator addded");
42-
assert.ok(await menuItems[3].$(".ui5-menu-item-icon-end"), "The third list item has sub-items and must have arrow right icon after the text");
43-
assert.ok(await menuItems[4].$(".ui5-menu-item-dummy-icon"), "The fourth list item has no icon and has dummy div instead of icon");
42+
assert.strictEqual(await menuItems[2].getAttribute("ui5-menu-separator"), "", "The third list item is a menu separator");
43+
assert.ok(await menuItems[3].$(".ui5-menu-item-icon-end"), "The fourth list item has sub-items and must have arrow right icon after the text");
44+
assert.ok(await menuItems[4].$(".ui5-menu-item-dummy-icon"), "The fifth list item has no icon and has dummy div instead of icon");
4445
});
4546

4647
it("Sub-menu creation, opening, closing and destroying", async () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
slug: ../../MenuSeparator
3+
---
4+
5+
<%COMPONENT_OVERVIEW%>
6+
7+
<%COMPONENT_METADATA%>

0 commit comments

Comments
 (0)