richard pushed to branch tor-browser-115.10.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: 00e009df by Henry Wilkes at 2024-04-18T13:57:12+01:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 42457: Add a loading icon for Lox invites.
Also reduce the amount of focus-jumping by keeping the focus on the disabled button.
Also change the focus of the last provideBridgeDialog page to be the table of bridges. NVDA did not announce the focus when it was set to the dialog itself.
- - - - -
6 changed files:
- browser/components/torpreferences/content/connectionPane.xhtml - browser/components/torpreferences/content/loxInviteDialog.js - browser/components/torpreferences/content/loxInviteDialog.xhtml - browser/components/torpreferences/content/provideBridgeDialog.js - browser/components/torpreferences/content/provideBridgeDialog.xhtml - browser/components/torpreferences/content/torPreferences.css
Changes:
===================================== browser/components/torpreferences/content/connectionPane.xhtml ===================================== @@ -48,7 +48,7 @@ class="network-status-label" data-l10n-id="tor-connection-internet-status-label" ></html:span> - <img alt="" class="network-status-loading-icon" /> + <img alt="" class="network-status-loading-icon tor-loading-icon" /> <html:span class="network-status-result"></html:span> </html:div> <html:button
===================================== browser/components/torpreferences/content/loxInviteDialog.js ===================================== @@ -62,12 +62,12 @@ const gLoxInvites = { this._remainingInvitesEl = document.getElementById( "lox-invite-dialog-remaining" ); + this._generateArea = document.getElementById( + "lox-invite-dialog-generate-area" + ); this._generateButton = document.getElementById( "lox-invite-dialog-generate-button" ); - this._connectingEl = document.getElementById( - "lox-invite-dialog-connecting" - ); this._errorEl = document.getElementById("lox-invite-dialog-error-message"); this._inviteListEl = document.getElementById("lox-invite-dialog-list");
@@ -237,20 +237,46 @@ const gLoxInvites = { _setGenerating(isGenerating) { this._generating = isGenerating; this._updateGenerateButtonState(); - this._connectingEl.classList.toggle("show-connecting", isGenerating); + this._generateArea.classList.toggle("show-connecting", isGenerating); },
+ /** + * Whether the generate button is disabled. + * + * @type {boolean} + */ + _generateDisabled: false, /** * Update the state of the generate button. */ _updateGenerateButtonState() { - this._generateButton.disabled = this._generating || !this._remainingInvites; + const disabled = this._generating || !this._remainingInvites; + this._generateDisabled = disabled; + // When generating we use "aria-disabled" rather than the "disabled" + // attribute so that the button can remain focusable whilst we generate + // invites. + // NOTE: When we generate the invite the focus will move to the invite list, + // so it should be safe to make the button non-focusable in this case. + const spoofDisabled = this._generating; + this._generateButton.disabled = disabled && !spoofDisabled; + this._generateButton.classList.toggle( + "spoof-button-disabled", + spoofDisabled + ); + if (spoofDisabled) { + this._generateButton.setAttribute("aria-disabled", "true"); + } else { + this._generateButton.removeAttribute("aria-disabled"); + } },
/** * Start generating a new invite. */ _generateNewInvite() { + if (this._generateDisabled) { + return; + } if (this._generating) { console.error("Already generating an invite"); return; @@ -258,15 +284,13 @@ const gLoxInvites = { this._setGenerating(true); // Clear the previous error. this._updateGenerateError(null); - // Move focus from the button to the connecting element, since button is - // now disabled. - this._connectingEl.focus();
- let lostFocus = false; + let moveFocus = false; Lox.generateInvite(this._loxId) .finally(() => { - // Fetch whether the connecting label still has focus before we hide it. - lostFocus = this._connectingEl.contains(document.activeElement); + // Fetch whether the generate button has focus before we potentially + // disable it. + moveFocus = this._generateButton.contains(document.activeElement); this._setGenerating(false); }) .then( @@ -279,7 +303,7 @@ const gLoxInvites = { this._inviteListEl.selectedIndex = 0; }
- if (lostFocus) { + if (moveFocus) { // Move focus to the new invite before we hide the "Connecting" // message. this._inviteListEl.focus(); @@ -295,12 +319,6 @@ const gLoxInvites = { this._updateGenerateError("generic"); break; } - - if (lostFocus) { - // Move focus back to the button before we hide the "Connecting" - // message. - this._generateButton.focus(); - } } ); }, @@ -315,7 +333,7 @@ const gLoxInvites = { // First clear the existing error. this._errorEl.removeAttribute("data-l10n-id"); this._errorEl.textContent = ""; - this._errorEl.classList.toggle("show-error", !!type); + this._generateArea.classList.toggle("show-error", !!type);
if (!type) { return;
===================================== browser/components/torpreferences/content/loxInviteDialog.xhtml ===================================== @@ -40,10 +40,14 @@ id="lox-invite-dialog-error-message" role="alert" ></html:span> + <img + id="lox-invite-dialog-loading-icon" + class="tor-loading-icon" + alt="" + /> <html:span id="lox-invite-dialog-connecting" role="alert" - tabindex="0" data-l10n-id="lox-invite-dialog-connecting" ></html:span> </html:div>
===================================== browser/components/torpreferences/content/provideBridgeDialog.js ===================================== @@ -84,13 +84,19 @@ const gProvideBridgeDialog = {
this._dialog = document.getElementById("user-provide-bridge-dialog"); this._acceptButton = this._dialog.getButton("accept"); + + // Inject our stylesheet into the shadow root so that the accept button can + // take the spoof-button-disabled styling. + const styleLink = document.createElement("link"); + styleLink.rel = "stylesheet"; + styleLink.href = + "chrome://browser/content/torpreferences/torPreferences.css"; + this._dialog.shadowRoot.append(styleLink); + this._textarea = document.getElementById("user-provide-bridge-textarea"); this._errorEl = document.getElementById( "user-provide-bridge-error-message" ); - this._connectingEl = document.getElementById( - "user-provide-bridge-connecting" - ); this._resultDescription = document.getElementById( "user-provide-result-description" ); @@ -152,13 +158,16 @@ const gProvideBridgeDialog = { * Reset focus position in the dialog. */ takeFocus() { - if (this._page === "entry") { - this._textarea.focus(); - } else { - // Move focus to the xul:window element. - // In particular, we do not want to keep the focus on the (same) accept - // button (with now different text). - document.documentElement.focus(); + switch (this._page) { + case "entry": + this._textarea.focus(); + break; + case "result": + // Move focus to the table. + // In particular, we do not want to keep the focus on the (same) accept + // button (with now different text). + this._bridgeGrid.focus(); + break; } },
@@ -193,12 +202,27 @@ const gProvideBridgeDialog = { } },
+ /** + * Whether the dialog accept button is disabled. + * + * @type {boolean} + */ + _acceptDisabled: false, /** * Callback for whenever the accept button's might need to be disabled. */ updateAcceptDisabled() { - this._acceptButton.disabled = + const disabled = this._page === "entry" && (this.isEmpty() || this._loxLoading); + this._acceptDisabled = disabled; + // Spoof the button to look and act as if it is disabled, but still allow + // keyboard focus so the user can sit on this button whilst we are loading. + this._acceptButton.classList.toggle("spoof-button-disabled", disabled); + if (disabled) { + this._acceptButton.setAttribute("aria-disabled", "true"); + } else { + this._acceptButton.removeAttribute("aria-disabled"); + } },
/** @@ -217,16 +241,7 @@ const gProvideBridgeDialog = { setLoxLoading(isLoading) { this._loxLoading = isLoading; this._textarea.readOnly = isLoading; - this._connectingEl.classList.toggle("show-connecting", isLoading); - if ( - isLoading && - this._acceptButton.contains( - this._acceptButton.getRootNode().activeElement - ) - ) { - // Move focus to the alert before we disable the button. - this._connectingEl.focus(); - } + this._dialog.classList.toggle("show-connecting", isLoading); this.updateAcceptDisabled(); },
@@ -236,6 +251,12 @@ const gProvideBridgeDialog = { * @param {Event} event - The dialogaccept event. */ onDialogAccept(event) { + if (this._acceptDisabled) { + // Prevent closing. + event.preventDefault(); + return; + } + if (this._page === "result") { this._result.accepted = true; // Continue to close the dialog. @@ -313,14 +334,11 @@ const gProvideBridgeDialog = { this._errorEl.textContent = ""; if (error) { this._textarea.setAttribute("aria-invalid", "true"); - // Move focus back to the text area, likely away from the Next button or - // the "Connecting..." alert. - this._textarea.focus(); } else { this._textarea.removeAttribute("aria-invalid"); } this._textarea.classList.toggle("invalid-input", !!error); - this._errorEl.classList.toggle("show-error", !!error); + this._dialog.classList.toggle("show-error", !!error);
if (!error) { return;
===================================== browser/components/torpreferences/content/provideBridgeDialog.xhtml ===================================== @@ -51,10 +51,14 @@ role="alert" aria-live="assertive" ></html:span> + <img + id="user-provide-bridge-loading-icon" + class="tor-loading-icon" + alt="" + /> <html:span id="user-provide-bridge-connecting" role="alert" - tabindex="0" data-l10n-id="user-provide-bridge-dialog-connecting" ></html:span> </html:div> @@ -70,6 +74,8 @@ id="user-provide-bridge-grid-display" class="tor-bridges-grid" role="table" + tabindex="0" + aria-labelledby="user-provide-result-description" ></html:div> <html:template id="user-provide-bridge-row-template"> <html:div class="tor-bridges-grid-row" role="row">
===================================== browser/components/torpreferences/content/torPreferences.css ===================================== @@ -14,6 +14,24 @@ button.spoof-button-disabled { pointer-events: none; }
+.tor-loading-icon { + width: 16px; + height: 16px; + content: image-set( + url("chrome://global/skin/icons/tor-light-loading.png"), + url("chrome://global/skin/icons/tor-light-loading@2x.png") 2x + ); +} + +@media (prefers-color-scheme: dark) { + .tor-loading-icon { + content: image-set( + url("chrome://global/skin/icons/tor-dark-loading.png"), + url("chrome://global/skin/icons/tor-dark-loading@2x.png") 2x + ); + } +} + /* Status */
#network-status-internet-area { @@ -81,21 +99,6 @@ button.spoof-button-disabled {
.network-status-loading-icon { margin-inline-end: 0.5em; - width: 16px; - height: 16px; - content: image-set( - url("chrome://global/skin/icons/tor-light-loading.png"), - url("chrome://global/skin/icons/tor-light-loading@2x.png") 2x - ); -} - -@media (prefers-color-scheme: dark) { - .network-status-loading-icon { - content: image-set( - url("chrome://global/skin/icons/tor-dark-loading.png"), - url("chrome://global/skin/icons/tor-dark-loading@2x.png") 2x - ); - } }
#network-status-internet-area:not(.status-loading) .network-status-loading-icon { @@ -900,6 +903,8 @@ dialog#torPreferences-requestBridge-dialog > hbox { #lox-invite-dialog-message-area { grid-area: message; justify-self: end; + display: flex; + align-items: center; }
#lox-invite-dialog-message-area::after { @@ -911,19 +916,29 @@ dialog#torPreferences-requestBridge-dialog > hbox { color: var(--in-content-error-text-color); }
-#lox-invite-dialog-error-message:not(.show-error) { +#lox-invite-dialog-generate-area:not(.show-error) #lox-invite-dialog-error-message { display: none; }
#lox-invite-dialog-connecting { color: var(--text-color-deemphasized); - /* TODO: Add spinner ::before */ + /* Gap with #user-provide-bridge-loading-icon. */ + margin-inline-start: 0.5em; }
-#lox-invite-dialog-connecting:not(.show-connecting) { +#lox-invite-dialog-generate-area:not(.show-connecting) #lox-invite-dialog-connecting { display: none; }
+#lox-invite-dialog-loading-icon { + flex: 0 0 auto; +} + +#lox-invite-dialog-generate-area:not(.show-connecting) #lox-invite-dialog-loading-icon { + /* Use width:0 to effectively hide, but still occupy vertical space. */ + width: 0; +} + #lox-invite-dialog-list-label { font-weight: 700; } @@ -1019,6 +1034,8 @@ groupbox#torPreferences-bridges-group textarea { flex: 0 0 auto; margin-block: 8px 12px; align-self: end; + display: flex; + align-items: center; }
#user-provide-bridge-message-area::after { @@ -1035,19 +1052,29 @@ groupbox#torPreferences-bridges-group textarea { color: var(--in-content-error-text-color); }
-#user-provide-bridge-error-message.not(.show-error) { +#user-provide-bridge-dialog:not(.show-error) #user-provide-bridge-error-message { display: none; }
#user-provide-bridge-connecting { color: var(--text-color-deemphasized); - /* TODO: Add spinner ::before */ + /* Gap with #user-provide-bridge-loading-icon. */ + margin-inline-start: 0.5em; }
-#user-provide-bridge-connecting:not(.show-connecting) { +#user-provide-bridge-dialog:not(.show-connecting) #user-provide-bridge-connecting { display: none; }
+#user-provide-bridge-loading-icon { + flex: 0 0 auto; +} + +#user-provide-bridge-dialog:not(.show-connecting) #user-provide-bridge-loading-icon { + /* Use width:0 to effectively hide, but still occupy vertical space. */ + width: 0; +} + #user-provide-bridge-result-page { flex: 1 1 0; min-height: 0; @@ -1065,6 +1092,11 @@ groupbox#torPreferences-bridges-group textarea { margin-block: 8px; }
+#user-provide-bridge-grid-display:focus-visible { + outline: var(--in-content-focus-outline); + outline-offset: var(--in-content-focus-outline-offset); +} + /* Connection settings dialog */ #torPreferences-connection-dialog label { /* Do not wrap the labels. */
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/00e009df...
tor-commits@lists.torproject.org