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