MenuItem.js (5701B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // A command in a menu. 8 9 const { 10 createFactory, 11 createRef, 12 PureComponent, 13 } = require("resource://devtools/client/shared/vendor/react.mjs"); 14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 16 const { button, li, span } = dom; 17 loader.lazyGetter(this, "Localized", () => 18 createFactory( 19 require("resource://devtools/client/shared/vendor/fluent-react.js") 20 .Localized 21 ) 22 ); 23 24 class MenuItem extends PureComponent { 25 static get propTypes() { 26 return { 27 // An optional keyboard shortcut to display next to the item. 28 // (This does not actually register the event listener for the key.) 29 accelerator: PropTypes.string, 30 31 // A tri-state value that may be true/false if item should be checkable, 32 // and undefined otherwise. 33 checked: PropTypes.bool, 34 35 // Any additional classes to assign to the button specified as 36 // a space-separated string. 37 className: PropTypes.string, 38 39 // A disabled state of the menu item. 40 disabled: PropTypes.bool, 41 42 // URL of the icon to associate with the MenuItem. (Optional) 43 // 44 // e.g. chrome://devtools/skim/image/foo.svg 45 // 46 // This may also be set in CSS using the --menuitem-icon-image variable. 47 // Note that in this case, the variable should specify the CSS <image> to 48 // use, not simply the URL (e.g. 49 // "url(chrome://devtools/skim/image/foo.svg)"). 50 icon: PropTypes.string, 51 52 // An optional ID to be assigned to the item. 53 id: PropTypes.string, 54 55 // The item label for use with legacy localization systems. 56 label: PropTypes.string, 57 58 // The Fluent ID for localizing the label. 59 l10nID: PropTypes.string, 60 61 // An optional callback to be invoked when the item is selected. 62 onClick: PropTypes.func, 63 64 // Optional menu item role override. Use this property with a value 65 // "menuitemradio" if the menu item is a radio. 66 role: PropTypes.string, 67 68 // An optional text for the item tooltip. 69 tooltip: PropTypes.string, 70 }; 71 } 72 73 /** 74 * Use this as a fallback `icon` prop if your MenuList contains MenuItems 75 * with or without icon in order to keep all MenuItems aligned. 76 */ 77 static get DUMMY_ICON() { 78 return `data:image/svg+xml,${encodeURIComponent( 79 '<svg height="16" width="16"></svg>' 80 )}`; 81 } 82 83 constructor(props) { 84 super(props); 85 this.labelRef = createRef(); 86 } 87 88 componentDidMount() { 89 if (!this.labelRef.current) { 90 return; 91 } 92 93 // Pre-fetch any backgrounds specified for the item. 94 const win = this.labelRef.current.ownerDocument.defaultView; 95 this.preloadCallback = win.requestIdleCallback(() => { 96 this.preloadCallback = null; 97 if (!this.labelRef.current) { 98 return; 99 } 100 101 const backgrounds = win 102 .getComputedStyle(this.labelRef.current, ":before") 103 .getCSSImageURLs("background-image"); 104 for (const background of backgrounds) { 105 const image = new Image(); 106 image.src = background; 107 } 108 }); 109 } 110 111 componentWillUnmount() { 112 if (!this.labelRef.current || !this.preloadCallback) { 113 return; 114 } 115 116 const win = this.labelRef.current.ownerDocument.defaultView; 117 if (win) { 118 win.cancelIdleCallback(this.preloadCallback); 119 } 120 this.preloadCallback = null; 121 } 122 123 render() { 124 const attr = { 125 className: "command", 126 }; 127 128 if (this.props.id) { 129 attr.id = this.props.id; 130 } 131 132 if (this.props.className) { 133 attr.className += " " + this.props.className; 134 } 135 136 if (this.props.icon) { 137 attr.className += " iconic"; 138 attr.style = { "--menuitem-icon-image": "url(" + this.props.icon + ")" }; 139 } 140 141 if (this.props.onClick) { 142 attr.onClick = this.props.onClick; 143 } 144 145 if (this.props.tooltip) { 146 attr.title = this.props.tooltip; 147 } 148 149 if (this.props.disabled) { 150 attr.disabled = this.props.disabled; 151 } 152 153 if (this.props.role) { 154 attr.role = this.props.role; 155 } else if (typeof this.props.checked !== "undefined") { 156 attr.role = "menuitemcheckbox"; 157 } else { 158 attr.role = "menuitem"; 159 } 160 161 if (this.props.checked) { 162 attr["aria-checked"] = true; 163 } 164 165 const children = []; 166 const className = "label"; 167 168 // Add the text label. 169 if (this.props.l10nID) { 170 // Fluent localized label. 171 children.push( 172 Localized( 173 { id: this.props.l10nID, key: "label" }, 174 span({ className, ref: this.labelRef }) 175 ) 176 ); 177 } else { 178 children.push( 179 span({ key: "label", className, ref: this.labelRef }, this.props.label) 180 ); 181 } 182 183 if (this.props.l10nID && this.props.label) { 184 console.warn( 185 "<MenuItem> should only take either an l10nID or a label, not both" 186 ); 187 } 188 if (!this.props.l10nID && !this.props.label) { 189 console.warn("<MenuItem> requires either an l10nID, or a label prop."); 190 } 191 192 if (typeof this.props.accelerator !== "undefined") { 193 const acceleratorLabel = span( 194 { key: "accelerator", className: "accelerator" }, 195 this.props.accelerator 196 ); 197 children.push(acceleratorLabel); 198 } 199 200 return li( 201 { 202 className: "menuitem", 203 role: "presentation", 204 }, 205 button(attr, children) 206 ); 207 } 208 } 209 210 module.exports = MenuItem;