securityLevelDialog.js (7624B)
1 "use strict"; 2 3 const { SecurityLevelPrefs } = ChromeUtils.importESModule( 4 "resource://gre/modules/SecurityLevel.sys.mjs" 5 ); 6 const { SecurityLevelUIUtils } = ChromeUtils.importESModule( 7 "resource:///modules/SecurityLevelUIUtils.sys.mjs" 8 ); 9 10 const gSecurityLevelDialog = { 11 /** 12 * The security level when this dialog was opened. 13 * 14 * @type {string} 15 */ 16 _prevLevel: SecurityLevelPrefs.securityLevelSummary, 17 /** 18 * The security level currently selected. 19 * 20 * @type {string} 21 */ 22 _selectedLevel: "", 23 /** 24 * The radiogroup for this preference. 25 * 26 * @type {?Element} 27 */ 28 _radiogroup: null, 29 /** 30 * A list of radio options and their containers. 31 * 32 * @type {?Array<{ container: Element, radio: Element }>} 33 */ 34 _radioOptions: null, 35 36 /** 37 * Initialise the dialog. 38 */ 39 async init() { 40 const dialog = document.getElementById("security-level-dialog"); 41 dialog.addEventListener("dialogaccept", event => { 42 event.preventDefault(); 43 if (this._acceptButton.disabled) { 44 return; 45 } 46 this._commitChange(); 47 }); 48 49 this._acceptButton = dialog.getButton("accept"); 50 51 document.l10n.setAttributes( 52 this._acceptButton, 53 "security-level-dialog-save-restart" 54 ); 55 56 this._radiogroup = document.getElementById("security-level-radiogroup"); 57 58 this._radioOptions = Array.from( 59 this._radiogroup.querySelectorAll(".security-level-radio-container"), 60 container => { 61 return { 62 container, 63 radio: container.querySelector(".security-level-radio"), 64 }; 65 } 66 ); 67 68 for (const { container, radio } of this._radioOptions) { 69 const level = radio.value; 70 radio.id = `security-level-radio-${level}`; 71 const currentEl = container.querySelector( 72 ".security-level-current-badge" 73 ); 74 currentEl.id = `security-level-current-badge-${level}`; 75 const descriptionEl = SecurityLevelUIUtils.createDescriptionElement( 76 level, 77 document 78 ); 79 descriptionEl.classList.add("indent"); 80 descriptionEl.id = `security-level-description-${level}`; 81 82 // Wait for the full translation of the element before adding it to the 83 // DOM. In particular, we want to make sure the elements have text before 84 // we measure the maxHeight below. 85 await document.l10n.translateFragment(descriptionEl); 86 document.l10n.pauseObserving(); 87 container.append(descriptionEl); 88 document.l10n.resumeObserving(); 89 90 if (level === this._prevLevel) { 91 currentEl.hidden = false; 92 // When the currentEl is visible, include it in the accessible name for 93 // the radio option. 94 // NOTE: The currentEl has an accessible name which includes punctuation 95 // to help separate it's content from the security level name. 96 // E.g. "Standard (Current level)". 97 radio.setAttribute("aria-labelledby", `${radio.id} ${currentEl.id}`); 98 } else { 99 currentEl.hidden = true; 100 } 101 // We point the accessible description to the wrapping 102 // .security-level-description element, rather than its children 103 // that define the actual text content. This means that when the 104 // privacy-extra-information is shown or hidden, its text content is 105 // included or excluded from the accessible description, respectively. 106 radio.setAttribute("aria-describedby", descriptionEl.id); 107 } 108 109 // We want to reserve the maximum height of the radiogroup so that the 110 // dialog has enough height when the user switches options. So we cycle 111 // through the options and measure the height when they are selected to set 112 // a minimum height that fits all of them. 113 // NOTE: At the time of implementation, at this point the dialog may not 114 // yet have the "subdialog" attribute, which means it is missing the 115 // common.css stylesheet from its shadow root, which effects the size of the 116 // .radio-check element and the font. Therefore, we have duplicated the 117 // import of common.css in SecurityLevelDialog.xhtml to ensure it is applied 118 // at this earlier stage. 119 let maxHeight = 0; 120 for (const { container } of this._radioOptions) { 121 container.classList.add("selected"); 122 maxHeight = Math.max( 123 maxHeight, 124 this._radiogroup.getBoundingClientRect().height 125 ); 126 container.classList.remove("selected"); 127 } 128 this._radiogroup.style.minHeight = `${maxHeight}px`; 129 130 if (this._prevLevel !== "custom") { 131 this._selectedLevel = this._prevLevel; 132 this._radiogroup.value = this._prevLevel; 133 } else { 134 this._radiogroup.selectedItem = null; 135 } 136 137 this._radiogroup.addEventListener("select", () => { 138 this._selectedLevel = this._radiogroup.value; 139 this._updateSelected(); 140 }); 141 142 this._updateSelected(); 143 }, 144 145 /** 146 * Update the UI in response to a change in selection. 147 */ 148 _updateSelected() { 149 this._acceptButton.disabled = 150 !this._selectedLevel || this._selectedLevel === this._prevLevel; 151 // Have the container's `selected` CSS class match the selection state of 152 // the radio elements. 153 for (const { container, radio } of this._radioOptions) { 154 container.classList.toggle("selected", radio.selected); 155 } 156 }, 157 158 /** 159 * Commit the change in security level and restart the browser. 160 */ 161 async _commitChange() { 162 const doNotWarnPref = "browser.security_level.disable_warn_before_restart"; 163 if (!Services.prefs.getBoolPref(doNotWarnPref, false)) { 164 const [titleString, bodyString, checkboxString, restartString] = 165 await document.l10n.formatValues([ 166 { id: "security-level-restart-warning-dialog-title" }, 167 { id: "security-level-restart-warning-dialog-body" }, 168 { id: "restart-warning-dialog-do-not-warn-checkbox" }, 169 { id: "restart-warning-dialog-restart-button" }, 170 ]); 171 const flags = 172 Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + 173 Services.prompt.BUTTON_POS_0_DEFAULT + 174 Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE + 175 Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL; 176 const propBag = await Services.prompt.asyncConfirmEx( 177 window.browsingContext.top, 178 Services.prompt.MODAL_TYPE_CONTENT, 179 titleString, 180 bodyString, 181 flags, 182 restartString, 183 null, 184 null, 185 checkboxString, 186 false, 187 { useTitle: true, noIcon: true } 188 ); 189 if (propBag.get("buttonNumClicked") !== 0) { 190 return; 191 } 192 if (propBag.get("checked")) { 193 Services.prefs.setBoolPref(doNotWarnPref, true); 194 } 195 } 196 SecurityLevelPrefs.setSecurityLevelBeforeRestart(this._selectedLevel); 197 Services.startup.quit( 198 Services.startup.eAttemptQuit | Services.startup.eRestart 199 ); 200 }, 201 }; 202 203 // Initial focus is not visible, even if opened with a keyboard. We avoid the 204 // default handler and manage the focus ourselves, which will paint the focus 205 // ring by default. 206 // NOTE: A side effect is that the focus ring will show even if the user opened 207 // with a mouse event. 208 // TODO: Remove this once bugzilla bug 1708261 is resolved. 209 document.subDialogSetDefaultFocus = () => { 210 document.getElementById("security-level-radiogroup").focus(); 211 }; 212 213 // Delay showing and sizing the subdialog until it is fully initialised. 214 document.mozSubdialogReady = new Promise(resolve => { 215 window.addEventListener( 216 "DOMContentLoaded", 217 () => { 218 gSecurityLevelDialog.init().finally(resolve); 219 }, 220 { once: true } 221 ); 222 });