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