xref: /aosp_15_r20/external/perfetto/ui/src/widgets/menu.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2023 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {classNames} from '../base/classnames';
17import {hasChildren} from '../base/mithril_utils';
18import {HTMLAttrs} from './common';
19import {Icon} from './icon';
20import {Popup, PopupAttrs, PopupPosition} from './popup';
21
22export interface MenuItemAttrs extends HTMLAttrs {
23  // Text to display on the menu button.
24  label: string;
25  // Optional left icon.
26  icon?: string;
27  // Optional right icon.
28  rightIcon?: string;
29  // Make the item appear greyed out block any interaction with it. No events
30  // will be fired.
31  // Defaults to false.
32  disabled?: boolean;
33  // Always show the button as if the "active" pseudo class were applied, which
34  // makes the button look permanently pressed.
35  // Useful for when the button represents some toggleable state, such as
36  // showing/hiding a popup menu.
37  // Defaults to false.
38  active?: boolean;
39  // If this menu item is a descendant of a popup, this setting means that
40  // clicking it will result in the popup being dismissed.
41  // Defaults to false when menuitem has children, true otherwise.
42  closePopupOnClick?: boolean;
43}
44
45// An interactive menu element with an icon.
46// If this node has children, a nested popup menu will be rendered.
47export class MenuItem implements m.ClassComponent<MenuItemAttrs> {
48  view(vnode: m.CVnode<MenuItemAttrs>): m.Children {
49    if (hasChildren(vnode)) {
50      return this.renderNested(vnode);
51    } else {
52      return this.renderSingle(vnode);
53    }
54  }
55
56  private renderNested({attrs, children}: m.CVnode<MenuItemAttrs>) {
57    const {
58      rightIcon = 'chevron_right',
59      closePopupOnClick = false,
60      ...rest
61    } = attrs;
62
63    return m(
64      PopupMenu2,
65      {
66        popupPosition: PopupPosition.RightStart,
67        trigger: m(MenuItem, {
68          rightIcon: rightIcon,
69          closePopupOnClick,
70          ...rest,
71        }),
72        showArrow: false,
73        createNewGroup: false,
74        edgeOffset: 5, // Adjust for popup padding & border.
75      },
76      children,
77    );
78  }
79
80  private renderSingle({attrs}: m.CVnode<MenuItemAttrs>) {
81    const {
82      label,
83      icon,
84      rightIcon,
85      disabled,
86      active,
87      closePopupOnClick = true,
88      className,
89      ...htmlAttrs
90    } = attrs;
91
92    const classes = classNames(
93      active && 'pf-active',
94      !disabled && closePopupOnClick && Popup.DISMISS_POPUP_GROUP_CLASS,
95      className,
96    );
97
98    return m(
99      'button.pf-menu-item' + (disabled ? '[disabled]' : ''),
100      {
101        ...htmlAttrs,
102        className: classes,
103      },
104      icon && m(Icon, {className: 'pf-left-icon', icon}),
105      rightIcon && m(Icon, {className: 'pf-right-icon', icon: rightIcon}),
106      label,
107    );
108  }
109}
110
111// An element which shows a dividing line between menu items.
112export class MenuDivider implements m.ClassComponent {
113  view() {
114    return m('.pf-menu-divider');
115  }
116}
117
118// A siple container for a menu.
119// The menu contents are passed in as children, and are typically MenuItems or
120// MenuDividers, but really they can be any Mithril component.
121export class Menu implements m.ClassComponent<HTMLAttrs> {
122  view({attrs, children}: m.CVnode<HTMLAttrs>) {
123    return m('.pf-menu', attrs, children);
124  }
125}
126
127interface PopupMenu2Attrs extends PopupAttrs {
128  // The trigger is mithril component which is used to toggle the popup when
129  // clicked, and provides the anchor on the page which the popup shall hover
130  // next to, and to which the popup's arrow shall point. The popup shall move
131  // around the page with this component, as if attached to it.
132  // This trigger can be any mithril component, but it is typically a Button,
133  // an Icon, or some other interactive component.
134  // Beware this element will have its `onclick`, `ref`, and `active` attributes
135  // overwritten.
136  // eslint-disable-next-line @typescript-eslint/no-explicit-any
137  trigger: m.Vnode<any, any>;
138  // Which side of the trigger to place to popup.
139  // Defaults to "bottom".
140  popupPosition?: PopupPosition;
141  // Whether we should show the little arrow pointing to the trigger.
142  // Defaults to true.
143  showArrow?: boolean;
144  // Whether this popup should form a new popup group.
145  // When nesting popups, grouping controls how popups are closed.
146  // When closing popups via the Escape key, each group is closed one by one,
147  // starting at the topmost group in the stack.
148  // When using a magic button to close groups (see DISMISS_POPUP_GROUP_CLASS),
149  // only the group in which the button lives and it's children will be closed.
150  // Defaults to true.
151  createNewGroup?: boolean;
152}
153
154// A combination of a Popup and a Menu component.
155// The menu contents are passed in as children, and are typically MenuItems or
156// MenuDividers, but really they can be any Mithril component.
157export class PopupMenu2 implements m.ClassComponent<PopupMenu2Attrs> {
158  view({attrs, children}: m.CVnode<PopupMenu2Attrs>) {
159    const {
160      trigger,
161      popupPosition = PopupPosition.Bottom,
162      ...popupAttrs
163    } = attrs;
164
165    return m(
166      Popup,
167      {
168        trigger,
169        position: popupPosition,
170        className: 'pf-popup-menu',
171        ...popupAttrs,
172      },
173      m(Menu, children),
174    );
175  }
176}
177