userMenu

userMenu отображает основную кнопку, по нажатию на которую открывается выпадающее меню со списком элементов.

XML-элемент

userMenu

Java-класс

UserMenu

Атрибуты

id - alignSelf - classNames - colspan - css - enabled - focusShortcut - openOnHover - overlayClass - tabIndex - themeNames - title - visible

Обработчики

AttachEvent - BlurEvent - DetachEvent - FocusEvent - UserChangedEvent - buttonRenderer - headerRenderer

Элементы

actionItem - componentItem - separator - textItem - viewItem

Основы

Основная кнопка отображает компоновку, представляющую текущего пользователя. Пользователь нажимает эту кнопку, чтобы открыть выпадающее меню.

Выпадающее меню содержит список действий, каждое из которых представлено кликабельным элементом или подменю.

user menu basic

Ниже приведен пример определения userMenu со стандартным рендерером кнопки и несколькими пунктами меню:

<userMenu>
    <items>
        <viewItem id="profileMenuItem" text="msg://profileMenuItem.text" icon="USER"
                  viewId="ProfileView"
                  themeNames="non-checkable"/>
        <viewItem id="settingsMenuItem" text="msg://settingsMenuItem.text" icon="COG"
                  viewClass="com.company.onboarding.view.settings.SettingsView"
                  themeNames="non-checkable"/>
        <actionItem id="themeMenuItem"
                    themeNames="non-checkable">
            <action id="themeMenuItem" type="userMenu_themeSwitch"/>
        </actionItem>
        <separator/>
        <actionItem id="logoutMenuItem" ref="logoutAction"
                    themeNames="non-checkable"/>
    </items>
</userMenu>

Элементы

userMenu, определенный в XML-дескрипторе, может содержать вложенные элементы:

actionItem

Элемент actionItem создает связь между пунктом пользовательского меню и конкретным action, который должен быть выполнен при щелчке по этому пункту.

Вы можете либо определить действие декларативно, либо использовать атрибут ref, чтобы сослаться на id уже определенного action.

<actions>
    <action id="logoutAction" text="msg:///actions.logout.text" type="logout"/>
</actions>
<layout>
    <userMenu id="userMenuActions">
        <items>
            <actionItem id="aboutMenuItem">
                <action id="aboutAction" text="msg://aboutAction.text" icon="INFO_CIRCLE_O"/> (1)
            </actionItem>
            <actionItem id="logoutMenuItem" ref="logoutAction"/> (2)
        </items>
    </userMenu>
</layout>
1 Декларативно определенное действие.
2 Ссылка на существующее действие.

Когда пользователь нажимает actionItem в выпадающем меню, Jmix автоматически вызывает action. Чтобы реализовать его логику, можно сгенерировать метод-обработчик ActionPerformedEvent:

@Autowired
private Notifications notifications;

@Subscribe("userMenuActions.aboutMenuItem.aboutAction")
public void onUserMenuActionsAboutMenuItemAboutAction(final ActionPerformedEvent event) {
    notifications.show("About");
}

Фреймворк предоставляет предопределенные User Menu Actions.

componentItem

Элемент componentItem позволяет определить пользовательское внутреннее содержимое для userMenu.

<userMenu id="userMenuComponent">
    <items>
        <componentItem id="emailItMenuItem">
            <hbox padding="false">
                <icon icon="MAILBOX"/>
                <span text="E Mail"/>
            </hbox>
        </componentItem>
    </items>
</userMenu>

С помощью Jmix Studio можно сгенерировать заготовку обработчика UserMenuItem.HasClickListener.ClickEvent для componentItem.

@Autowired
private Notifications notifications;

@Subscribe("userMenuComponent.emailItMenuItem")
public void onUserMenuComponentEmailItMenuItemClick(final UserMenuItem.HasClickListener.ClickEvent<ComponentUserMenuItem> event) {
    notifications.show("Email: test@river.net");
}

textItem

Элемент textItem содержит текст и значок.

<userMenu id="userMenuText">
    <items>
        <textItem id="contactUsMenuItem" text="msg://contactUsItem.text" icon="PHONE"/>
    </items>
</userMenu>

С помощью Jmix Studio можно сгенерировать заготовку обработчика UserMenuItem.HasClickListener.ClickEvent для textItem.

@Autowired
private Notifications notifications;

@Subscribe("userMenuText.contactUsMenuItem")
public void onUserMenuTextContactUsMenuItemClick(final UserMenuItem.HasClickListener.ClickEvent<TextUserMenuItem> event) {
    notifications.show("Phone number: +6(876)5463");
}

viewItem

Элемент viewItem позволяет открыть определенный экран.

<userMenu id="userMenuView">
    <items>
        <viewItem id="profileMenuItem" text="msg://profileMenuItem.text" icon="USER"
                  viewId="ProfileView"/> (1)
        <viewItem id="settingsMenuItem" text="msg://settingsMenuItem.text" icon="COG"
                  viewClass="com.company.onboarding.view.settings.SettingsView"
                  openMode="DIALOG"/> (2)
    </items>
</userMenu>
1 Открывает экран с указанным идентификатором.
2 Открывает экран указанного класса в режиме DIALOG.

separator

Элемент separator используется для визуального разделения элементов в выпадающем меню.

Разделители автоматически скрываются при следующих условиях:

  1. Несколько разделителей идут подряд. В этом случае видимым остается только один разделитель.

  2. Разделитель является первым дочерним элементом, а рендерер заголовка не определен.

  3. Разделитель является последним дочерним элементом.

Вложенные элементы

Элементы textItem и componentItem поддерживают вложенные элементы, что позволяет определить иерархическое меню.

user menu nested
<userMenu>
    <items>
        <textItem id="helpMenuItem"
                  text="msg://helpMenuItem.text" icon="QUESTION_CIRCLE"
                  themeNames="non-checkable">
            <items>
                <textItem id="documentationMenuItem" text="msg://documentationMenuItem.text"/>
                <textItem id="aboutMenuItem" text="msg://aboutMenuItem.text"/>
            </items>
        </textItem>
    </items>
</userMenu>

Варианты темы

Используйте атрибут themeNames для настройки темы компонента.

Вариант Описание Поддерживается в

tertiary

Удаляет фон у основной кнопки.

Aura, Lumo

non-checkable

Удаляет область, используемую для отображения флажка выбора.

Aura, Lumo

Тему non-checkable можно применять как к компоненту userMenu, так и к отдельным пунктам меню.

Если подменю содержит элементы с флажками выбора, применяйте non-checkable отдельно к каждому элементу первого уровня.

Рендереры

Фреймворк позволяет гибко настраивать содержимое кнопки и необязательного заголовка меню.

Рендерер кнопки

Рендерер кнопки можно задать либо методом setButtonRenderer(), либо аннотацией @Install.

user menu button renderer
import com.company.onboarding.entity.User;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.server.streams.DownloadResponse;
import io.jmix.core.FileRef;
import io.jmix.core.FileStorage;
import io.jmix.flowui.UiComponents;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;

// class declaration and annotations omitted

    @Autowired
    private UiComponents uiComponents;
    @Autowired
    private FileStorage fileStorage;

    @Install(to = "userMenu", subject = "buttonRenderer")
    private Component userMenuButtonRenderer(final UserDetails userDetails) {
        if (!(userDetails instanceof User user)) {
            return null;
        }

        String userName = generateUserName(user);
        Avatar avatar = createAvatar(userName, user.getPicture());
        Span name = uiComponents.create(Span.class);
        name.setText(userName);
        name.addClassName(LumoUtility.TextColor.BODY);

        HorizontalLayout content = uiComponents.create(HorizontalLayout.class);
        content.setAlignItems(FlexComponent.Alignment.CENTER);
        content.add(avatar, name);
        content.addClassNames( (1)
                LumoUtility.Padding.Horizontal.MEDIUM,
                LumoUtility.Padding.Vertical.SMALL);

        return content;
    }

    private String generateUserName(User user) {
        String userName = String.format("%s %s",
                        Strings.nullToEmpty(user.getFirstName()),
                        Strings.nullToEmpty(user.getLastName()))
                .trim();

        return userName.isEmpty() ? user.getUsername() : userName;
    }

    private Avatar createAvatar(String fullName, @Nullable FileRef fileRef) {
        Avatar avatar = uiComponents.create(Avatar.class); (2)
        avatar.setName(fullName); (3)
        avatar.getElement().setAttribute("tabindex", "-1"); (4)

        if (fileRef != null) {
            avatar.setImageHandler( (5)
                    DownloadHandler.fromInputStream(event ->
                            new DownloadResponse(
                                    fileStorage.openStream(fileRef),
                                    fileRef.getFileName(),
                                    fileRef.getContentType(),
                                    -1
                            )
                    )
            );
        }

        return avatar;
    }
1 Стандартные отступы HorizontalLayout слишком велики, поэтому мы добавляем нужные отступы с помощью стилей.
2 Экземпляр компонента Avatar создается с помощью фабрики UiComponents.
3 Полное имя пользователя передается в Avatar, чтобы сгенерировать резервные инициалы, если изображение недоступно.
4 avatar по умолчанию получает фокус, но не предоставляет Java API для его отключения. Удалите tabindex, чтобы фокус мог получать только компонент userMenu.
5 DownloadHandler передает изображение аватара из файлового хранилища по ссылке, сохраненной в атрибуте picture сущности User.

Рендерер заголовка

Рендерер заголовка определяет содержимое, отображаемое в верхней части меню. Его можно задать либо методом setHeaderRenderer(), либо аннотацией @Install.

user menu header renderer
import com.company.onboarding.entity.User;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.server.streams.DownloadResponse;
import io.jmix.core.FileRef;
import io.jmix.core.FileStorage;
import io.jmix.flowui.UiComponents;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;

// class declaration and annotations omitted

    @Autowired
    private UiComponents uiComponents;
    @Autowired
    private FileStorage fileStorage;

    @Install(to = "userMenu", subject = "headerRenderer")
    private Component userMenuHeaderRenderer(final UserDetails userDetails) {
        if (!(userDetails instanceof User user)) {
            return null;
        }

        String name = generateUserName(user);

        Avatar avatar = createAvatar(name, user.getPicture());
        avatar.addThemeVariants(AvatarVariant.LUMO_LARGE);
        avatar.addClassName("user-menu-avatar");

        Span text = uiComponents.create(Span.class);
        text.setText(name);
        text.setClassName("user-menu-text");

        Div content = uiComponents.create(Div.class);
        content.setClassName("user-menu-header-content"); (1)
        content.add(avatar, text);

        if (name.equals(user.getUsername())) {
            text.addClassNames("user-menu-text-subtext");
        } else {
            Span subtext = uiComponents.create(Span.class);
            subtext.setText(user.getUsername());
            subtext.setClassName("user-menu-subtext");

            content.add(subtext);
        }

        return content;
    }

    private String generateUserName(User user) {
        String userName = String.format("%s %s",
                        Strings.nullToEmpty(user.getFirstName()),
                        Strings.nullToEmpty(user.getLastName()))
                .trim();

        return userName.isEmpty() ? user.getUsername() : userName;
    }

    private Avatar createAvatar(String fullName, @Nullable FileRef fileRef) {
        Avatar avatar = uiComponents.create(Avatar.class); (2)
        avatar.setName(fullName); (3)
        avatar.getElement().setAttribute("tabindex", "-1"); (4)

        if (fileRef != null) {
            avatar.setImageHandler( (5)
                    DownloadHandler.fromInputStream(event ->
                            new DownloadResponse(
                                    fileStorage.openStream(fileRef),
                                    fileRef.getFileName(),
                                    fileRef.getContentType(),
                                    -1
                            )
                    )
            );
        }

        return avatar;
    }
1 Позиции компонентов задаются с помощью стилей.
2 Экземпляр компонента Avatar создается с помощью фабрики UiComponents.
3 Полное имя пользователя передается в Avatar, чтобы сгенерировать резервные инициалы, если изображение недоступно.
4 avatar по умолчанию получает фокус, но не предоставляет Java API для его отключения. Удалите tabindex, чтобы фокус мог получать только компонент userMenu.
5 DownloadHandler передает изображение аватара из файлового хранилища по ссылке, сохраненной в атрибуте picture сущности User.
.user-menu-header-content {
    display: grid;
    grid-template: "avatar text"
                   "avatar subtext";
    grid-template-columns: auto 1fr;
    column-gap: var(--lumo-space-s);

    width: 100%;
    box-sizing: border-box;

    color: var(--lumo-body-text-color);
    padding: var(--lumo-space-xs) var(--lumo-space-l) var(--lumo-space-xs) var(--lumo-space-s);
}

.user-menu-header-content > .user-menu-avatar {
    grid-area: avatar;
    align-self: center;
}

.user-menu-header-content > .user-menu-text {
    grid-area: text;

    color: var(--lumo-body-text-color);
    font-weight: 700;
    font-size: var(--lumo-font-size-m);
}

.user-menu-header-content > .user-menu-text-subtext {
    grid-row: text / subtext;
}

.user-menu-header-content > .user-menu-text {
    align-self: center;
    text-align: start;

    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

.user-menu-header-content > .user-menu-subtext {
    grid-area: subtext;
    align-self: center;
    text-align: start;

    color: var(--lumo-secondary-text-color);
    font-size: var(--lumo-font-size-xs);

    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

Атрибуты

В Jmix существует множество общих атрибутов, которые выполняют одну и ту же функцию для всех компонентов. Ниже приведены атрибуты, специфичные для userMenu:

Название

Описание

Значение по умолчанию

openOnHover

Если атрибут openOnHover установлен в true, выпадающий список элементов будет открываться автоматически, когда поле получает фокус с помощью мыши или касания.

false

Обработчики

В Jmix существует множество общих обработчиков, которые настраиваются одинаково для всех компонентов. Ниже приведены обработчики, специфичные для userMenu:

Чтобы сгенерировать заглушку обработчика в Jmix Studio, используйте вкладку Handlers панели инспектора Jmix UI, или команду Generate Handler, доступную на верхней панели контроллера экрана и через меню CodeGenerate (Alt+Insert / Cmd+N).

Название

Описание

UserChangedEvent

Событие io.jmix.flowui.kit.component.usermenu.JmixUserMenu.UserChangedEvent возникает, когда меняется пользователь, связанный с компонентом пользовательского меню.

buttonRenderer

Устанавливает Renderer, отвечающий за отображение содержимого кнопки на основе текущего пользователя. Смотрите Рендерер кнопки.

headerRenderer

Устанавливает Renderer, отвечающий за отображение содержимого заголовка на основе текущего пользователя. Смотрите Рендерер заголовка.