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
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:
... | ... | @@ -48,7 +48,7 @@ |
48 | 48 | class="network-status-label"
|
49 | 49 | data-l10n-id="tor-connection-internet-status-label"
|
50 | 50 | ></html:span>
|
51 | - <img alt="" class="network-status-loading-icon" />
|
|
51 | + <img alt="" class="network-status-loading-icon tor-loading-icon" />
|
|
52 | 52 | <html:span class="network-status-result"></html:span>
|
53 | 53 | </html:div>
|
54 | 54 | <html:button
|
... | ... | @@ -62,12 +62,12 @@ const gLoxInvites = { |
62 | 62 | this._remainingInvitesEl = document.getElementById(
|
63 | 63 | "lox-invite-dialog-remaining"
|
64 | 64 | );
|
65 | + this._generateArea = document.getElementById(
|
|
66 | + "lox-invite-dialog-generate-area"
|
|
67 | + );
|
|
65 | 68 | this._generateButton = document.getElementById(
|
66 | 69 | "lox-invite-dialog-generate-button"
|
67 | 70 | );
|
68 | - this._connectingEl = document.getElementById(
|
|
69 | - "lox-invite-dialog-connecting"
|
|
70 | - );
|
|
71 | 71 | this._errorEl = document.getElementById("lox-invite-dialog-error-message");
|
72 | 72 | this._inviteListEl = document.getElementById("lox-invite-dialog-list");
|
73 | 73 | |
... | ... | @@ -237,20 +237,46 @@ const gLoxInvites = { |
237 | 237 | _setGenerating(isGenerating) {
|
238 | 238 | this._generating = isGenerating;
|
239 | 239 | this._updateGenerateButtonState();
|
240 | - this._connectingEl.classList.toggle("show-connecting", isGenerating);
|
|
240 | + this._generateArea.classList.toggle("show-connecting", isGenerating);
|
|
241 | 241 | },
|
242 | 242 | |
243 | + /**
|
|
244 | + * Whether the generate button is disabled.
|
|
245 | + *
|
|
246 | + * @type {boolean}
|
|
247 | + */
|
|
248 | + _generateDisabled: false,
|
|
243 | 249 | /**
|
244 | 250 | * Update the state of the generate button.
|
245 | 251 | */
|
246 | 252 | _updateGenerateButtonState() {
|
247 | - this._generateButton.disabled = this._generating || !this._remainingInvites;
|
|
253 | + const disabled = this._generating || !this._remainingInvites;
|
|
254 | + this._generateDisabled = disabled;
|
|
255 | + // When generating we use "aria-disabled" rather than the "disabled"
|
|
256 | + // attribute so that the button can remain focusable whilst we generate
|
|
257 | + // invites.
|
|
258 | + // NOTE: When we generate the invite the focus will move to the invite list,
|
|
259 | + // so it should be safe to make the button non-focusable in this case.
|
|
260 | + const spoofDisabled = this._generating;
|
|
261 | + this._generateButton.disabled = disabled && !spoofDisabled;
|
|
262 | + this._generateButton.classList.toggle(
|
|
263 | + "spoof-button-disabled",
|
|
264 | + spoofDisabled
|
|
265 | + );
|
|
266 | + if (spoofDisabled) {
|
|
267 | + this._generateButton.setAttribute("aria-disabled", "true");
|
|
268 | + } else {
|
|
269 | + this._generateButton.removeAttribute("aria-disabled");
|
|
270 | + }
|
|
248 | 271 | },
|
249 | 272 | |
250 | 273 | /**
|
251 | 274 | * Start generating a new invite.
|
252 | 275 | */
|
253 | 276 | _generateNewInvite() {
|
277 | + if (this._generateDisabled) {
|
|
278 | + return;
|
|
279 | + }
|
|
254 | 280 | if (this._generating) {
|
255 | 281 | console.error("Already generating an invite");
|
256 | 282 | return;
|
... | ... | @@ -258,15 +284,13 @@ const gLoxInvites = { |
258 | 284 | this._setGenerating(true);
|
259 | 285 | // Clear the previous error.
|
260 | 286 | this._updateGenerateError(null);
|
261 | - // Move focus from the button to the connecting element, since button is
|
|
262 | - // now disabled.
|
|
263 | - this._connectingEl.focus();
|
|
264 | 287 | |
265 | - let lostFocus = false;
|
|
288 | + let moveFocus = false;
|
|
266 | 289 | Lox.generateInvite(this._loxId)
|
267 | 290 | .finally(() => {
|
268 | - // Fetch whether the connecting label still has focus before we hide it.
|
|
269 | - lostFocus = this._connectingEl.contains(document.activeElement);
|
|
291 | + // Fetch whether the generate button has focus before we potentially
|
|
292 | + // disable it.
|
|
293 | + moveFocus = this._generateButton.contains(document.activeElement);
|
|
270 | 294 | this._setGenerating(false);
|
271 | 295 | })
|
272 | 296 | .then(
|
... | ... | @@ -279,7 +303,7 @@ const gLoxInvites = { |
279 | 303 | this._inviteListEl.selectedIndex = 0;
|
280 | 304 | }
|
281 | 305 | |
282 | - if (lostFocus) {
|
|
306 | + if (moveFocus) {
|
|
283 | 307 | // Move focus to the new invite before we hide the "Connecting"
|
284 | 308 | // message.
|
285 | 309 | this._inviteListEl.focus();
|
... | ... | @@ -295,12 +319,6 @@ const gLoxInvites = { |
295 | 319 | this._updateGenerateError("generic");
|
296 | 320 | break;
|
297 | 321 | }
|
298 | - |
|
299 | - if (lostFocus) {
|
|
300 | - // Move focus back to the button before we hide the "Connecting"
|
|
301 | - // message.
|
|
302 | - this._generateButton.focus();
|
|
303 | - }
|
|
304 | 322 | }
|
305 | 323 | );
|
306 | 324 | },
|
... | ... | @@ -315,7 +333,7 @@ const gLoxInvites = { |
315 | 333 | // First clear the existing error.
|
316 | 334 | this._errorEl.removeAttribute("data-l10n-id");
|
317 | 335 | this._errorEl.textContent = "";
|
318 | - this._errorEl.classList.toggle("show-error", !!type);
|
|
336 | + this._generateArea.classList.toggle("show-error", !!type);
|
|
319 | 337 | |
320 | 338 | if (!type) {
|
321 | 339 | return;
|
... | ... | @@ -40,10 +40,14 @@ |
40 | 40 | id="lox-invite-dialog-error-message"
|
41 | 41 | role="alert"
|
42 | 42 | ></html:span>
|
43 | + <img
|
|
44 | + id="lox-invite-dialog-loading-icon"
|
|
45 | + class="tor-loading-icon"
|
|
46 | + alt=""
|
|
47 | + />
|
|
43 | 48 | <html:span
|
44 | 49 | id="lox-invite-dialog-connecting"
|
45 | 50 | role="alert"
|
46 | - tabindex="0"
|
|
47 | 51 | data-l10n-id="lox-invite-dialog-connecting"
|
48 | 52 | ></html:span>
|
49 | 53 | </html:div>
|
... | ... | @@ -84,13 +84,19 @@ const gProvideBridgeDialog = { |
84 | 84 | |
85 | 85 | this._dialog = document.getElementById("user-provide-bridge-dialog");
|
86 | 86 | this._acceptButton = this._dialog.getButton("accept");
|
87 | + |
|
88 | + // Inject our stylesheet into the shadow root so that the accept button can
|
|
89 | + // take the spoof-button-disabled styling.
|
|
90 | + const styleLink = document.createElement("link");
|
|
91 | + styleLink.rel = "stylesheet";
|
|
92 | + styleLink.href =
|
|
93 | + "chrome://browser/content/torpreferences/torPreferences.css";
|
|
94 | + this._dialog.shadowRoot.append(styleLink);
|
|
95 | + |
|
87 | 96 | this._textarea = document.getElementById("user-provide-bridge-textarea");
|
88 | 97 | this._errorEl = document.getElementById(
|
89 | 98 | "user-provide-bridge-error-message"
|
90 | 99 | );
|
91 | - this._connectingEl = document.getElementById(
|
|
92 | - "user-provide-bridge-connecting"
|
|
93 | - );
|
|
94 | 100 | this._resultDescription = document.getElementById(
|
95 | 101 | "user-provide-result-description"
|
96 | 102 | );
|
... | ... | @@ -152,13 +158,16 @@ const gProvideBridgeDialog = { |
152 | 158 | * Reset focus position in the dialog.
|
153 | 159 | */
|
154 | 160 | takeFocus() {
|
155 | - if (this._page === "entry") {
|
|
156 | - this._textarea.focus();
|
|
157 | - } else {
|
|
158 | - // Move focus to the <xul:window> element.
|
|
159 | - // In particular, we do not want to keep the focus on the (same) accept
|
|
160 | - // button (with now different text).
|
|
161 | - document.documentElement.focus();
|
|
161 | + switch (this._page) {
|
|
162 | + case "entry":
|
|
163 | + this._textarea.focus();
|
|
164 | + break;
|
|
165 | + case "result":
|
|
166 | + // Move focus to the table.
|
|
167 | + // In particular, we do not want to keep the focus on the (same) accept
|
|
168 | + // button (with now different text).
|
|
169 | + this._bridgeGrid.focus();
|
|
170 | + break;
|
|
162 | 171 | }
|
163 | 172 | },
|
164 | 173 | |
... | ... | @@ -193,12 +202,27 @@ const gProvideBridgeDialog = { |
193 | 202 | }
|
194 | 203 | },
|
195 | 204 | |
205 | + /**
|
|
206 | + * Whether the dialog accept button is disabled.
|
|
207 | + *
|
|
208 | + * @type {boolean}
|
|
209 | + */
|
|
210 | + _acceptDisabled: false,
|
|
196 | 211 | /**
|
197 | 212 | * Callback for whenever the accept button's might need to be disabled.
|
198 | 213 | */
|
199 | 214 | updateAcceptDisabled() {
|
200 | - this._acceptButton.disabled =
|
|
215 | + const disabled =
|
|
201 | 216 | this._page === "entry" && (this.isEmpty() || this._loxLoading);
|
217 | + this._acceptDisabled = disabled;
|
|
218 | + // Spoof the button to look and act as if it is disabled, but still allow
|
|
219 | + // keyboard focus so the user can sit on this button whilst we are loading.
|
|
220 | + this._acceptButton.classList.toggle("spoof-button-disabled", disabled);
|
|
221 | + if (disabled) {
|
|
222 | + this._acceptButton.setAttribute("aria-disabled", "true");
|
|
223 | + } else {
|
|
224 | + this._acceptButton.removeAttribute("aria-disabled");
|
|
225 | + }
|
|
202 | 226 | },
|
203 | 227 | |
204 | 228 | /**
|
... | ... | @@ -217,16 +241,7 @@ const gProvideBridgeDialog = { |
217 | 241 | setLoxLoading(isLoading) {
|
218 | 242 | this._loxLoading = isLoading;
|
219 | 243 | this._textarea.readOnly = isLoading;
|
220 | - this._connectingEl.classList.toggle("show-connecting", isLoading);
|
|
221 | - if (
|
|
222 | - isLoading &&
|
|
223 | - this._acceptButton.contains(
|
|
224 | - this._acceptButton.getRootNode().activeElement
|
|
225 | - )
|
|
226 | - ) {
|
|
227 | - // Move focus to the alert before we disable the button.
|
|
228 | - this._connectingEl.focus();
|
|
229 | - }
|
|
244 | + this._dialog.classList.toggle("show-connecting", isLoading);
|
|
230 | 245 | this.updateAcceptDisabled();
|
231 | 246 | },
|
232 | 247 | |
... | ... | @@ -236,6 +251,12 @@ const gProvideBridgeDialog = { |
236 | 251 | * @param {Event} event - The dialogaccept event.
|
237 | 252 | */
|
238 | 253 | onDialogAccept(event) {
|
254 | + if (this._acceptDisabled) {
|
|
255 | + // Prevent closing.
|
|
256 | + event.preventDefault();
|
|
257 | + return;
|
|
258 | + }
|
|
259 | + |
|
239 | 260 | if (this._page === "result") {
|
240 | 261 | this._result.accepted = true;
|
241 | 262 | // Continue to close the dialog.
|
... | ... | @@ -313,14 +334,11 @@ const gProvideBridgeDialog = { |
313 | 334 | this._errorEl.textContent = "";
|
314 | 335 | if (error) {
|
315 | 336 | this._textarea.setAttribute("aria-invalid", "true");
|
316 | - // Move focus back to the text area, likely away from the Next button or
|
|
317 | - // the "Connecting..." alert.
|
|
318 | - this._textarea.focus();
|
|
319 | 337 | } else {
|
320 | 338 | this._textarea.removeAttribute("aria-invalid");
|
321 | 339 | }
|
322 | 340 | this._textarea.classList.toggle("invalid-input", !!error);
|
323 | - this._errorEl.classList.toggle("show-error", !!error);
|
|
341 | + this._dialog.classList.toggle("show-error", !!error);
|
|
324 | 342 | |
325 | 343 | if (!error) {
|
326 | 344 | return;
|
... | ... | @@ -51,10 +51,14 @@ |
51 | 51 | role="alert"
|
52 | 52 | aria-live="assertive"
|
53 | 53 | ></html:span>
|
54 | + <img
|
|
55 | + id="user-provide-bridge-loading-icon"
|
|
56 | + class="tor-loading-icon"
|
|
57 | + alt=""
|
|
58 | + />
|
|
54 | 59 | <html:span
|
55 | 60 | id="user-provide-bridge-connecting"
|
56 | 61 | role="alert"
|
57 | - tabindex="0"
|
|
58 | 62 | data-l10n-id="user-provide-bridge-dialog-connecting"
|
59 | 63 | ></html:span>
|
60 | 64 | </html:div>
|
... | ... | @@ -70,6 +74,8 @@ |
70 | 74 | id="user-provide-bridge-grid-display"
|
71 | 75 | class="tor-bridges-grid"
|
72 | 76 | role="table"
|
77 | + tabindex="0"
|
|
78 | + aria-labelledby="user-provide-result-description"
|
|
73 | 79 | ></html:div>
|
74 | 80 | <html:template id="user-provide-bridge-row-template">
|
75 | 81 | <html:div class="tor-bridges-grid-row" role="row">
|
... | ... | @@ -14,6 +14,24 @@ button.spoof-button-disabled { |
14 | 14 | pointer-events: none;
|
15 | 15 | }
|
16 | 16 | |
17 | +.tor-loading-icon {
|
|
18 | + width: 16px;
|
|
19 | + height: 16px;
|
|
20 | + content: image-set(
|
|
21 | + url("chrome://global/skin/icons/tor-light-loading.png"),
|
|
22 | + url("chrome://global/skin/icons/tor-light-loading@2x.png") 2x
|
|
23 | + );
|
|
24 | +}
|
|
25 | + |
|
26 | +@media (prefers-color-scheme: dark) {
|
|
27 | + .tor-loading-icon {
|
|
28 | + content: image-set(
|
|
29 | + url("chrome://global/skin/icons/tor-dark-loading.png"),
|
|
30 | + url("chrome://global/skin/icons/tor-dark-loading@2x.png") 2x
|
|
31 | + );
|
|
32 | + }
|
|
33 | +}
|
|
34 | + |
|
17 | 35 | /* Status */
|
18 | 36 | |
19 | 37 | #network-status-internet-area {
|
... | ... | @@ -81,21 +99,6 @@ button.spoof-button-disabled { |
81 | 99 | |
82 | 100 | .network-status-loading-icon {
|
83 | 101 | margin-inline-end: 0.5em;
|
84 | - width: 16px;
|
|
85 | - height: 16px;
|
|
86 | - content: image-set(
|
|
87 | - url("chrome://global/skin/icons/tor-light-loading.png"),
|
|
88 | - url("chrome://global/skin/icons/tor-light-loading@2x.png") 2x
|
|
89 | - );
|
|
90 | -}
|
|
91 | - |
|
92 | -@media (prefers-color-scheme: dark) {
|
|
93 | - .network-status-loading-icon {
|
|
94 | - content: image-set(
|
|
95 | - url("chrome://global/skin/icons/tor-dark-loading.png"),
|
|
96 | - url("chrome://global/skin/icons/tor-dark-loading@2x.png") 2x
|
|
97 | - );
|
|
98 | - }
|
|
99 | 102 | }
|
100 | 103 | |
101 | 104 | #network-status-internet-area:not(.status-loading) .network-status-loading-icon {
|
... | ... | @@ -900,6 +903,8 @@ dialog#torPreferences-requestBridge-dialog > hbox { |
900 | 903 | #lox-invite-dialog-message-area {
|
901 | 904 | grid-area: message;
|
902 | 905 | justify-self: end;
|
906 | + display: flex;
|
|
907 | + align-items: center;
|
|
903 | 908 | }
|
904 | 909 | |
905 | 910 | #lox-invite-dialog-message-area::after {
|
... | ... | @@ -911,19 +916,29 @@ dialog#torPreferences-requestBridge-dialog > hbox { |
911 | 916 | color: var(--in-content-error-text-color);
|
912 | 917 | }
|
913 | 918 | |
914 | -#lox-invite-dialog-error-message:not(.show-error) {
|
|
919 | +#lox-invite-dialog-generate-area:not(.show-error) #lox-invite-dialog-error-message {
|
|
915 | 920 | display: none;
|
916 | 921 | }
|
917 | 922 | |
918 | 923 | #lox-invite-dialog-connecting {
|
919 | 924 | color: var(--text-color-deemphasized);
|
920 | - /* TODO: Add spinner ::before */
|
|
925 | + /* Gap with #user-provide-bridge-loading-icon. */
|
|
926 | + margin-inline-start: 0.5em;
|
|
921 | 927 | }
|
922 | 928 | |
923 | -#lox-invite-dialog-connecting:not(.show-connecting) {
|
|
929 | +#lox-invite-dialog-generate-area:not(.show-connecting) #lox-invite-dialog-connecting {
|
|
924 | 930 | display: none;
|
925 | 931 | }
|
926 | 932 | |
933 | +#lox-invite-dialog-loading-icon {
|
|
934 | + flex: 0 0 auto;
|
|
935 | +}
|
|
936 | + |
|
937 | +#lox-invite-dialog-generate-area:not(.show-connecting) #lox-invite-dialog-loading-icon {
|
|
938 | + /* Use width:0 to effectively hide, but still occupy vertical space. */
|
|
939 | + width: 0;
|
|
940 | +}
|
|
941 | + |
|
927 | 942 | #lox-invite-dialog-list-label {
|
928 | 943 | font-weight: 700;
|
929 | 944 | }
|
... | ... | @@ -1019,6 +1034,8 @@ groupbox#torPreferences-bridges-group textarea { |
1019 | 1034 | flex: 0 0 auto;
|
1020 | 1035 | margin-block: 8px 12px;
|
1021 | 1036 | align-self: end;
|
1037 | + display: flex;
|
|
1038 | + align-items: center;
|
|
1022 | 1039 | }
|
1023 | 1040 | |
1024 | 1041 | #user-provide-bridge-message-area::after {
|
... | ... | @@ -1035,19 +1052,29 @@ groupbox#torPreferences-bridges-group textarea { |
1035 | 1052 | color: var(--in-content-error-text-color);
|
1036 | 1053 | }
|
1037 | 1054 | |
1038 | -#user-provide-bridge-error-message.not(.show-error) {
|
|
1055 | +#user-provide-bridge-dialog:not(.show-error) #user-provide-bridge-error-message {
|
|
1039 | 1056 | display: none;
|
1040 | 1057 | }
|
1041 | 1058 | |
1042 | 1059 | #user-provide-bridge-connecting {
|
1043 | 1060 | color: var(--text-color-deemphasized);
|
1044 | - /* TODO: Add spinner ::before */
|
|
1061 | + /* Gap with #user-provide-bridge-loading-icon. */
|
|
1062 | + margin-inline-start: 0.5em;
|
|
1045 | 1063 | }
|
1046 | 1064 | |
1047 | -#user-provide-bridge-connecting:not(.show-connecting) {
|
|
1065 | +#user-provide-bridge-dialog:not(.show-connecting) #user-provide-bridge-connecting {
|
|
1048 | 1066 | display: none;
|
1049 | 1067 | }
|
1050 | 1068 | |
1069 | +#user-provide-bridge-loading-icon {
|
|
1070 | + flex: 0 0 auto;
|
|
1071 | +}
|
|
1072 | + |
|
1073 | +#user-provide-bridge-dialog:not(.show-connecting) #user-provide-bridge-loading-icon {
|
|
1074 | + /* Use width:0 to effectively hide, but still occupy vertical space. */
|
|
1075 | + width: 0;
|
|
1076 | +}
|
|
1077 | + |
|
1051 | 1078 | #user-provide-bridge-result-page {
|
1052 | 1079 | flex: 1 1 0;
|
1053 | 1080 | min-height: 0;
|
... | ... | @@ -1065,6 +1092,11 @@ groupbox#torPreferences-bridges-group textarea { |
1065 | 1092 | margin-block: 8px;
|
1066 | 1093 | }
|
1067 | 1094 | |
1095 | +#user-provide-bridge-grid-display:focus-visible {
|
|
1096 | + outline: var(--in-content-focus-outline);
|
|
1097 | + outline-offset: var(--in-content-focus-outline-offset);
|
|
1098 | +}
|
|
1099 | + |
|
1068 | 1100 | /* Connection settings dialog */
|
1069 | 1101 | #torPreferences-connection-dialog label {
|
1070 | 1102 | /* Do not wrap the labels. */
|