Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-menu): selectable menu items #10028

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Conversation

NHristov-sap
Copy link
Contributor

@NHristov-sap NHristov-sap commented Oct 15, 2024

This PR introduces MenuItemGroup component that can hold regular MenuItem components. The MenuItemGroup has a property named itemSelectionMode which can have values among None (default), SingleSelect and MultiSelect. MenuItemGroup can be slotted in a Menu or MenuItem default slot as any other regular MenuItem. Nesting of MenuItemGroups is not allowed, but any Menu or MenuItem can contain more than one MenuItemGroup components with different itemSelectionMode settings.

When itemSelectionMode is:

  • None, the Menu acts exactly like until now.
  • SingleSelect means that zero or one MenuItems can be selected at a time.
  • MultiSelect means that zero or many MenuItems can be selected at a time.

There is also new property isSelected introduced in MenuItem. By setting it the item is marked as selected and this is visualized as checkmark at the end of the item. This property is taken into account only when the corresponding item is a member of MenuItemGroup with SingleSelect or MultiSelect value of itemSelectionMode.

image

It is recommended to place separators before and after each MenuItemGroup, but this is not mandatory and depends on the application developers.

packages/main/src/MenuItemGroup.ts Show resolved Hide resolved
packages/main/src/MenuItemGroup.hbs Show resolved Hide resolved
packages/main/src/MenuItemGroup.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItemGroup.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved
@NHristov-sap NHristov-sap requested a review from hinzzx October 18, 2024 12:17
Copy link
Contributor

@nnaydenow nnaydenow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking unti: #10070

@nnaydenow nnaydenow dismissed their stale review October 22, 2024 13:52

Menu tests are fixed and this PR no longer need to be blocked

packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/main/src/Menu.ts Outdated Show resolved Hide resolved

this.items.forEach(item => {
if (item.isGroup) {
items.push(...(item as MenuItemGroup)._menuItems);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me filtration of menu items should happen here since it doesn't know this context and simply items property to be used.

Copy link
Contributor Author

@NHristov-sap NHristov-sap Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group also can contain separators, which are not regular itrems, that's why I want to be safe and use MenuItemGroup's _menuItems getter which returns only regular items

packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Show resolved Hide resolved
* @private
*/
@property()
_itemSelectionMode: `${ItemSelectionMode}` = ItemSelectionMode.None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selection mode for consistency with all other components that provide selection

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MenuItem extends ListItem (where selectionMode already exists, but with different behavior, so mixing is not possible here.

* **Note:** A selected `ui5-menu-item` have selection mark displayed ad its end.
* @default false
* @public
* @since 2.4.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since 2.5.0

why do we need setter and getter? they do synchronous property change and it should be used in very edge cases.

overall why do we need isSelected property which leads to inconsistency with other components that provide selection? what is the difference between selected and isSelected overall?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setter is needed because if you select item that is inside a MenuItemGroup with Single selection mode, other selected items should be deselected automatically by the group. Here we fire item-selection event that is handled by group and if it is necessary (selection mode is Single) the group unselects other items.

As I already wrote as answer of Ilhan's comment, the MenuItem extends ListItem that also have selection property, but it is related to totally different behavior, so selection here is different and we need different property for this. selected in ListItem does this:

image

and isSelected in MenuItem t=does this:

image

and at the same time we can have "original" ListItem selected state in Menu like here:

image

packages/main/src/MenuItem.ts Outdated Show resolved Hide resolved
* @public
*/
@property()
itemSelectionMode: `${ItemSelectionMode}` = ItemSelectionMode.None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in the MenuItem.

Add also since tag and use plain string as initializer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, selectionMode belongs to ListItem with different meaning and behavior
Regarding the @since - the whole component is new, and already have @since tag.

packages/main/src/MenuItemGroup.ts Show resolved Hide resolved
packages/main/src/types/ItemSelectionMode.ts Show resolved Hide resolved
_itemClick(e: CustomEvent<ListItemClickEventDetail>) {
const item = e.detail.item as MenuItem;
const prevSelected = item.isSelected;

item.isSelected = !prevSelected;
Copy link
Member

@ilhan007 ilhan007 Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the MenuItem needs a new property "selected", you will also benefit from it in the styles as you will be able to use [selected] selector and not adding some additional classes. And you won't need isSelected as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MenuItem extends ListItem which already have selected property, but selected in ListItem has totally different behavior, so I can neither use selected nor reuse related CSS. So I definitely need another "selected" state property here.

Copy link
Contributor

@tsanislavgatev tsanislavgatev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me with the exception of some namings, but i can't think of a better ones.

packages/main/src/Menu.ts Outdated Show resolved Hide resolved
packages/main/src/MenuItem.ts Show resolved Hide resolved
* @since 2.5.0
*/
@property({ type: Boolean })
set isSelected(value: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSelected is not ok for a property name. It must be selected. Let's discuss refactoring possibilities to make selected not clash with the list item one.

*/
@property({ type: Boolean })
set isSelected(value: boolean) {
this.fireDecoratorEvent("item-selection");
Copy link
Contributor

@vladitasev vladitasev Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must only fire events upon user interaction, not when the application sets a property.

Also, custom setters are an anti-pattern, and are only accepted in extreme cases where synchronous behavior is needed (f.e. the Popover opener/open property that must open the popover in the same tick for the iOS soft keyboard to work).

Remove the custom setter, make it a normal property, and fire the event only when the user manually selects/deselects a menu item (and not when the application sets it).

const items: MenuItem[] = [];

this.items.forEach(item => {
if (item.isGroup) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

item.isGroup is not a proper typescript check, therefore you need as MenuItemGroup on the next line. Same for other parts of the code. The correct way to do it is for example as done in Side Navigation:

	get isSideNavigationItemBase() {
		return true;
	}

...............

const isInstanceOfSideNavigationItemBase = (object: any): object is SideNavigationItemBase => {
	return "isSideNavigationItemBase" in object;
};

You must make a similar instanceof function for MenuItem/Group etc... and then when you do the same check as on line 363, Typescript will know that if the check passed, the type of your item is whatever you put in is, as shown above.

packages/main/src/MenuItemGroup.ts Show resolved Hide resolved
}

get _menuItems() {
return this.items.filter((item) : item is MenuItem => !item.isSeparator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's good, you used is so typescript knows what _menuItems is.

packages/main/src/Menu.ts Show resolved Hide resolved
@github-actions github-actions bot added the Stale label Jan 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants