| ... | 
... | 
@@ -32,7 +32,6 @@ class AboutTorConnect { | 
| 
32
 | 
32
 | 
   selectors = Object.freeze({
 | 
| 
33
 | 
33
 | 
     textContainer: {
 | 
| 
34
 | 
34
 | 
       title: "div.title",
  | 
| 
35
 | 
 
 | 
-      titleText: "h1.title-text",
  | 
| 
36
 | 
35
 | 
       longContentText: "#connectLongContentText",
  | 
| 
37
 | 
36
 | 
     },
  | 
| 
38
 | 
37
 | 
     progress: {
 | 
| ... | 
... | 
@@ -77,7 +76,7 @@ class AboutTorConnect { | 
| 
77
 | 
76
 | 
 
  | 
| 
78
 | 
77
 | 
   elements = Object.freeze({
 | 
| 
79
 | 
78
 | 
     title: document.querySelector(this.selectors.textContainer.title),
  | 
| 
80
 | 
 
 | 
-    titleText: document.querySelector(this.selectors.textContainer.titleText),
  | 
| 
 
 | 
79
 | 
+    heading: document.getElementById("tor-connect-heading"),
 | 
| 
81
 | 
80
 | 
     longContentText: document.querySelector(
  | 
| 
82
 | 
81
 | 
       this.selectors.textContainer.longContentText
  | 
| 
83
 | 
82
 | 
     ),
  | 
| ... | 
... | 
@@ -138,18 +137,44 @@ class AboutTorConnect { | 
| 
138
 | 
137
 | 
 
  | 
| 
139
 | 
138
 | 
   locations = {};
 | 
| 
140
 | 
139
 | 
 
  | 
| 
141
 | 
 
 | 
-  beginBootstrapping() {
 | 
| 
142
 | 
 
 | 
-    RPMSendAsyncMessage("torconnect:begin-bootstrapping", {});
 | 
| 
 
 | 
140
 | 
+  /**
  | 
| 
 
 | 
141
 | 
+   * Whether the user requested a cancellation of the bootstrap from *this*
  | 
| 
 
 | 
142
 | 
+   * page.
  | 
| 
 
 | 
143
 | 
+   *
  | 
| 
 
 | 
144
 | 
+   * @type {boolean}
 | 
| 
 
 | 
145
 | 
+   */
  | 
| 
 
 | 
146
 | 
+  userCancelled = false;
  | 
| 
 
 | 
147
 | 
+
  | 
| 
 
 | 
148
 | 
+  /**
  | 
| 
 
 | 
149
 | 
+   * Start a normal bootstrap attempt.
  | 
| 
 
 | 
150
 | 
+   *
  | 
| 
 
 | 
151
 | 
+   * @param {boolean} userClickedConnect - Whether this request was triggered by
 | 
| 
 
 | 
152
 | 
+   *   the user clicking the "Connect" button on the "Start" page.
  | 
| 
 
 | 
153
 | 
+   */
  | 
| 
 
 | 
154
 | 
+  beginBootstrapping(userClickedConnect) {
 | 
| 
 
 | 
155
 | 
+    RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
 | 
| 
 
 | 
156
 | 
+      userClickedConnect,
  | 
| 
 
 | 
157
 | 
+    });
  | 
| 
143
 | 
158
 | 
   }
  | 
| 
144
 | 
159
 | 
 
  | 
| 
 
 | 
160
 | 
+  /**
  | 
| 
 
 | 
161
 | 
+   * Start an auto bootstrap attempt.
  | 
| 
 
 | 
162
 | 
+   *
  | 
| 
 
 | 
163
 | 
+   * @param {string} regionCode - The region code to use for the bootstrap, or
 | 
| 
 
 | 
164
 | 
+   *   "automatic".
  | 
| 
 
 | 
165
 | 
+   */
  | 
| 
145
 | 
166
 | 
   beginAutoBootstrapping(regionCode) {
 | 
| 
146
 | 
167
 | 
     RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
 | 
| 
147
 | 
168
 | 
       regionCode,
  | 
| 
148
 | 
169
 | 
     });
  | 
| 
149
 | 
170
 | 
   }
  | 
| 
150
 | 
171
 | 
 
  | 
| 
 
 | 
172
 | 
+  /**
  | 
| 
 
 | 
173
 | 
+   * Try and cancel the current bootstrap attempt.
  | 
| 
 
 | 
174
 | 
+   */
  | 
| 
151
 | 
175
 | 
   cancelBootstrapping() {
 | 
| 
152
 | 
176
 | 
     RPMSendAsyncMessage("torconnect:cancel-bootstrapping");
 | 
| 
 
 | 
177
 | 
+    this.userCancelled = true;
  | 
| 
153
 | 
178
 | 
   }
  | 
| 
154
 | 
179
 | 
 
  | 
| 
155
 | 
180
 | 
   /*
  | 
| ... | 
... | 
@@ -260,7 +285,7 @@ class AboutTorConnect { | 
| 
260
 | 
285
 | 
   }
  | 
| 
261
 | 
286
 | 
 
  | 
| 
262
 | 
287
 | 
   setTitle(title, className) {
 | 
| 
263
 | 
 
 | 
-    this.elements.titleText.textContent = title;
  | 
| 
 
 | 
288
 | 
+    this.elements.heading.textContent = title;
  | 
| 
264
 | 
289
 | 
     this.elements.title.className = "title";
  | 
| 
265
 | 
290
 | 
     if (className) {
 | 
| 
266
 | 
291
 | 
       this.elements.title.classList.add(className);
  | 
| ... | 
... | 
@@ -349,18 +374,88 @@ class AboutTorConnect { | 
| 
349
 | 
374
 | 
     }
  | 
| 
350
 | 
375
 | 
   }
  | 
| 
351
 | 
376
 | 
 
  | 
| 
 
 | 
377
 | 
+  /**
  | 
| 
 
 | 
378
 | 
+   * The connect button that was focused just prior to a bootstrap attempt, if
  | 
| 
 
 | 
379
 | 
+   * any.
  | 
| 
 
 | 
380
 | 
+   *
  | 
| 
 
 | 
381
 | 
+   * @type {?Element}
 | 
| 
 
 | 
382
 | 
+   */
  | 
| 
 
 | 
383
 | 
+  preBootstrappingFocus = null;
  | 
| 
 
 | 
384
 | 
+
  | 
| 
 
 | 
385
 | 
+  /**
  | 
| 
 
 | 
386
 | 
+   * The stage that was shown on this page just prior to a bootstrap attempt.
  | 
| 
 
 | 
387
 | 
+   *
  | 
| 
 
 | 
388
 | 
+   * @type {?string}
 | 
| 
 
 | 
389
 | 
+   */
  | 
| 
 
 | 
390
 | 
+  preBootstrappingStage = null;
  | 
| 
 
 | 
391
 | 
+
  | 
| 
352
 | 
392
 | 
   /*
  | 
| 
353
 | 
393
 | 
   These methods update the UI based on the current TorConnect state
  | 
| 
354
 | 
394
 | 
   */
  | 
| 
355
 | 
395
 | 
 
  | 
| 
356
 | 
 
 | 
-  updateStage(stage) {
 | 
| 
 
 | 
396
 | 
+  /**
  | 
| 
 
 | 
397
 | 
+   * Update the shown stage.
  | 
| 
 
 | 
398
 | 
+   *
  | 
| 
 
 | 
399
 | 
+   * @param {ConnectStage} stage - The new stage to show.
 | 
| 
 
 | 
400
 | 
+   * @param {boolean} [focusConnect=false] - Whether to try and focus the
 | 
| 
 
 | 
401
 | 
+   *   connect button, if we are in the Start stage.
  | 
| 
 
 | 
402
 | 
+   */
  | 
| 
 
 | 
403
 | 
+  updateStage(stage, focusConnect = false) {
 | 
| 
357
 | 
404
 | 
     if (stage.name === this.shownStage) {
 | 
| 
358
 | 
405
 | 
       return;
  | 
| 
359
 | 
406
 | 
     }
  | 
| 
360
 | 
407
 | 
 
  | 
| 
 
 | 
408
 | 
+    const prevStage = this.shownStage;
  | 
| 
361
 | 
409
 | 
     this.shownStage = stage.name;
  | 
| 
362
 | 
410
 | 
     this.selectedLocation = stage.defaultRegion;
  | 
| 
363
 | 
411
 | 
 
  | 
| 
 
 | 
412
 | 
+    // By default we want to reset the focus to the top of the page when
  | 
| 
 
 | 
413
 | 
+    // changing the displayed page since we want a user to read the new page
  | 
| 
 
 | 
414
 | 
+    // before activating a control.
  | 
| 
 
 | 
415
 | 
+    let moveFocus = this.elements.heading;
  | 
| 
 
 | 
416
 | 
+
  | 
| 
 
 | 
417
 | 
+    if (stage.name === "Bootstrapping") {
 | 
| 
 
 | 
418
 | 
+      this.preBootstrappingStage = prevStage;
  | 
| 
 
 | 
419
 | 
+      this.preBootstrappingFocus = null;
  | 
| 
 
 | 
420
 | 
+      if (focusConnect && stage.isQuickstart) {
 | 
| 
 
 | 
421
 | 
+        // If this is the initial automatic bootstrap triggered by the
  | 
| 
 
 | 
422
 | 
+        // quickstart preference, treat as if the previous shown stage was
  | 
| 
 
 | 
423
 | 
+        // "Start" and the user clicked the "Connect" button.
  | 
| 
 
 | 
424
 | 
+        // Then, if the user cancels, the focus should still move to the
  | 
| 
 
 | 
425
 | 
+        // "Connect" button.
  | 
| 
 
 | 
426
 | 
+        this.preBootstrappingStage = "Start";
  | 
| 
 
 | 
427
 | 
+        this.preBootstrappingFocus = this.elements.connectButton;
  | 
| 
 
 | 
428
 | 
+      } else if (this.elements.connectButton.contains(document.activeElement)) {
 | 
| 
 
 | 
429
 | 
+        this.preBootstrappingFocus = this.elements.connectButton;
  | 
| 
 
 | 
430
 | 
+      } else if (
  | 
| 
 
 | 
431
 | 
+        this.elements.tryBridgeButton.contains(document.activeElement)
  | 
| 
 
 | 
432
 | 
+      ) {
 | 
| 
 
 | 
433
 | 
+        this.preBootstrappingFocus = this.elements.tryBridgeButton;
  | 
| 
 
 | 
434
 | 
+      }
  | 
| 
 
 | 
435
 | 
+    } else {
 | 
| 
 
 | 
436
 | 
+      if (
  | 
| 
 
 | 
437
 | 
+        this.userCancelled &&
  | 
| 
 
 | 
438
 | 
+        prevStage === "Bootstrapping" &&
  | 
| 
 
 | 
439
 | 
+        stage.name === this.preBootstrappingStage &&
  | 
| 
 
 | 
440
 | 
+        this.preBootstrappingFocus &&
  | 
| 
 
 | 
441
 | 
+        this.elements.cancelButton.contains(document.activeElement)
  | 
| 
 
 | 
442
 | 
+      ) {
 | 
| 
 
 | 
443
 | 
+        // If returning back to the same stage after the user tried to cancel
  | 
| 
 
 | 
444
 | 
+        // bootstrapping from within this page, then we restore the focus to the
  | 
| 
 
 | 
445
 | 
+        // connect button to allow the user to quickly re-try.
  | 
| 
 
 | 
446
 | 
+        // If the bootstrap was cancelled for any other reason, we reset the
  | 
| 
 
 | 
447
 | 
+        // focus as usual.
  | 
| 
 
 | 
448
 | 
+        moveFocus = this.preBootstrappingFocus;
  | 
| 
 
 | 
449
 | 
+      }
  | 
| 
 
 | 
450
 | 
+      // Clear the Bootstrapping variables.
  | 
| 
 
 | 
451
 | 
+      this.preBootstrappingStage = null;
  | 
| 
 
 | 
452
 | 
+      this.preBootstrappingFocus = null;
  | 
| 
 
 | 
453
 | 
+    }
  | 
| 
 
 | 
454
 | 
+
  | 
| 
 
 | 
455
 | 
+    // Clear the recording of the cancellation request.
  | 
| 
 
 | 
456
 | 
+    this.userCancelled = false;
  | 
| 
 
 | 
457
 | 
+
  | 
| 
 
 | 
458
 | 
+    let isLoaded = true;
  | 
| 
364
 | 
459
 | 
     let showProgress = false;
  | 
| 
365
 | 
460
 | 
     let showLog = false;
  | 
| 
366
 | 
461
 | 
     switch (stage.name) {
 | 
| ... | 
... | 
@@ -368,14 +463,21 @@ class AboutTorConnect { | 
| 
368
 | 
463
 | 
         console.error("Should not be open when TorConnect is disabled");
 | 
| 
369
 | 
464
 | 
         break;
  | 
| 
370
 | 
465
 | 
       case "Loading":
  | 
| 
 
 | 
466
 | 
+        // Unexpected for this page to open so early.
  | 
| 
 
 | 
467
 | 
+        console.warn("Page opened whilst loading");
 | 
| 
 
 | 
468
 | 
+        isLoaded = false;
  | 
| 
 
 | 
469
 | 
+        break;
  | 
| 
371
 | 
470
 | 
       case "Start":
  | 
| 
372
 | 
 
 | 
-        // Loading is not currnetly handled, treat the same as "Start", but UI
  | 
| 
373
 | 
 
 | 
-        // will be unresponsive.
  | 
| 
374
 | 
471
 | 
         this.showStart(stage.tryAgain, stage.potentiallyBlocked);
  | 
| 
 
 | 
472
 | 
+        if (focusConnect) {
 | 
| 
 
 | 
473
 | 
+          moveFocus = this.elements.connectButton;
  | 
| 
 
 | 
474
 | 
+        }
  | 
| 
375
 | 
475
 | 
         break;
  | 
| 
376
 | 
476
 | 
       case "Bootstrapping":
  | 
| 
377
 | 
477
 | 
         showProgress = true;
  | 
| 
378
 | 
478
 | 
         this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain);
  | 
| 
 
 | 
479
 | 
+        // Always focus the cancel button.
  | 
| 
 
 | 
480
 | 
+        moveFocus = this.elements.cancelButton;
  | 
| 
379
 | 
481
 | 
         break;
  | 
| 
380
 | 
482
 | 
       case "Offline":
  | 
| 
381
 | 
483
 | 
         showLog = true;
  | 
| ... | 
... | 
@@ -419,6 +521,9 @@ class AboutTorConnect { | 
| 
419
 | 
521
 | 
     } else {
 | 
| 
420
 | 
522
 | 
       this.hide(this.elements.viewLogButton);
  | 
| 
421
 | 
523
 | 
     }
  | 
| 
 
 | 
524
 | 
+
  | 
| 
 
 | 
525
 | 
+    document.body.classList.toggle("loaded", isLoaded);
 | 
| 
 
 | 
526
 | 
+    moveFocus.focus();
  | 
| 
422
 | 
527
 | 
   }
  | 
| 
423
 | 
528
 | 
 
  | 
| 
424
 | 
529
 | 
   updateBootstrappingStatus(data) {
 | 
| ... | 
... | 
@@ -452,10 +557,9 @@ class AboutTorConnect { | 
| 
452
 | 
557
 | 
     this.show(this.elements.quickstartContainer);
  | 
| 
453
 | 
558
 | 
     this.show(this.elements.configureButton);
  | 
| 
454
 | 
559
 | 
     this.show(this.elements.connectButton, true);
  | 
| 
455
 | 
 
 | 
-    this.elements.connectButton.focus();
  | 
| 
456
 | 
 
 | 
-    if (tryAgain) {
 | 
| 
457
 | 
 
 | 
-      this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
  | 
| 
458
 | 
 
 | 
-    }
  | 
| 
 
 | 
560
 | 
+    this.elements.connectButton.textContent = tryAgain
  | 
| 
 
 | 
561
 | 
+      ? TorStrings.torConnect.tryAgain
  | 
| 
 
 | 
562
 | 
+      : TorStrings.torConnect.torConnectButton;
  | 
| 
459
 | 
563
 | 
     if (potentiallyBlocked) {
 | 
| 
460
 | 
564
 | 
       this.setBreadcrumbsStatus(
  | 
| 
461
 | 
565
 | 
         BreadcrumbStatus.Active,
  | 
| ... | 
... | 
@@ -511,7 +615,6 @@ class AboutTorConnect { | 
| 
511
 | 
615
 | 
     }
  | 
| 
512
 | 
616
 | 
     this.hideButtons();
  | 
| 
513
 | 
617
 | 
     this.show(this.elements.cancelButton);
  | 
| 
514
 | 
 
 | 
-    this.elements.cancelButton.focus();
  | 
| 
515
 | 
618
 | 
   }
  | 
| 
516
 | 
619
 | 
 
  | 
| 
517
 | 
620
 | 
   showOffline() {
 | 
| ... | 
... | 
@@ -541,7 +644,6 @@ class AboutTorConnect { | 
| 
541
 | 
644
 | 
       BreadcrumbStatus.Disabled
  | 
| 
542
 | 
645
 | 
     );
  | 
| 
543
 | 
646
 | 
     this.showLocationForm(true, TorStrings.torConnect.tryBridge);
  | 
| 
544
 | 
 
 | 
-    this.elements.tryBridgeButton.focus();
  | 
| 
545
 | 
647
 | 
   }
  | 
| 
546
 | 
648
 | 
 
  | 
| 
547
 | 
649
 | 
   showRegionNotFound() {
 | 
| ... | 
... | 
@@ -557,7 +659,6 @@ class AboutTorConnect { | 
| 
557
 | 
659
 | 
       BreadcrumbStatus.Disabled
  | 
| 
558
 | 
660
 | 
     );
  | 
| 
559
 | 
661
 | 
     this.showLocationForm(false, TorStrings.torConnect.tryBridge);
  | 
| 
560
 | 
 
 | 
-    this.elements.tryBridgeButton.focus();
  | 
| 
561
 | 
662
 | 
   }
  | 
| 
562
 | 
663
 | 
 
  | 
| 
563
 | 
664
 | 
   showConfirmRegion(error) {
 | 
| ... | 
... | 
@@ -573,7 +674,6 @@ class AboutTorConnect { | 
| 
573
 | 
674
 | 
       BreadcrumbStatus.Active
  | 
| 
574
 | 
675
 | 
     );
  | 
| 
575
 | 
676
 | 
     this.showLocationForm(false, TorStrings.torConnect.tryAgain);
  | 
| 
576
 | 
 
 | 
-    this.elements.tryBridgeButton.focus();
  | 
| 
577
 | 
677
 | 
   }
  | 
| 
578
 | 
678
 | 
 
  | 
| 
579
 | 
679
 | 
   showFinalError(error) {
 | 
| ... | 
... | 
@@ -722,7 +822,8 @@ class AboutTorConnect { | 
| 
722
 | 
822
 | 
     this.elements.connectButton.textContent =
  | 
| 
723
 | 
823
 | 
       TorStrings.torConnect.torConnectButton;
  | 
| 
724
 | 
824
 | 
     this.elements.connectButton.addEventListener("click", () => {
 | 
| 
725
 | 
 
 | 
-      this.beginBootstrapping();
  | 
| 
 
 | 
825
 | 
+      // Record as userClickedConnect if we are in the Start stage.
  | 
| 
 
 | 
826
 | 
+      this.beginBootstrapping(this.shownStage === "Start");
  | 
| 
726
 | 
827
 | 
     });
  | 
| 
727
 | 
828
 | 
 
  | 
| 
728
 | 
829
 | 
     this.populateLocations();
  | 
| ... | 
... | 
@@ -802,7 +903,13 @@ class AboutTorConnect { | 
| 
802
 | 
903
 | 
     this.initObservers();
  | 
| 
803
 | 
904
 | 
     this.initKeyboardShortcuts();
  | 
| 
804
 | 
905
 | 
 
  | 
| 
805
 | 
 
 | 
-    this.updateStage(args.stage);
  | 
| 
 
 | 
906
 | 
+    // If we have previously opened about:torconnect and the user tried the
  | 
| 
 
 | 
907
 | 
+    // "Connect" button we want to focus the "Connect" button for easy
  | 
| 
 
 | 
908
 | 
+    // activation.
  | 
| 
 
 | 
909
 | 
+    // Otherwise, we do not want to focus it for first time users so they can
  | 
| 
 
 | 
910
 | 
+    // read the full page first.
  | 
| 
 
 | 
911
 | 
+    const focusConnect = args.userHasEverClickedConnect;
  | 
| 
 
 | 
912
 | 
+    this.updateStage(args.stage, focusConnect);
  | 
| 
806
 | 
913
 | 
     this.updateQuickstart(args.quickstartEnabled);
  | 
| 
807
 | 
914
 | 
   }
  | 
| 
808
 | 
915
 | 
 }
  |