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