@authsignal/browser 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -105,18 +105,6 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
105
105
  PERFORMANCE OF THIS SOFTWARE.
106
106
  ***************************************************************************** */
107
107
 
108
- function __rest(s, e) {
109
- var t = {};
110
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
111
- t[p] = s[p];
112
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
113
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
114
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
115
- t[p[i]] = s[p[i]];
116
- }
117
- return t;
118
- }
119
-
120
108
  function __awaiter(thisArg, _arguments, P, generator) {
121
109
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
122
110
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -543,15 +531,15 @@ var PasskeyApiClient = /** @class */ (function () {
543
531
  this.baseUrl = baseUrl;
544
532
  }
545
533
  PasskeyApiClient.prototype.registrationOptions = function (_a) {
546
- var token = _a.token, userName = _a.userName, authenticatorAttachment = _a.authenticatorAttachment;
534
+ var token = _a.token, username = _a.username, authenticatorAttachment = _a.authenticatorAttachment;
547
535
  return __awaiter(this, void 0, void 0, function () {
548
536
  var body, response;
549
537
  return __generator(this, function (_b) {
550
538
  switch (_b.label) {
551
539
  case 0:
552
540
  body = Boolean(authenticatorAttachment)
553
- ? { username: userName, authenticatorAttachment: authenticatorAttachment }
554
- : { username: userName };
541
+ ? { username: username, authenticatorAttachment: authenticatorAttachment }
542
+ : { username: username };
555
543
  response = fetch("".concat(this.baseUrl, "/client/user-authenticators/passkey/registration-options"), {
556
544
  method: "POST",
557
545
  headers: this.buildHeaders(token),
@@ -564,16 +552,17 @@ var PasskeyApiClient = /** @class */ (function () {
564
552
  });
565
553
  };
566
554
  PasskeyApiClient.prototype.authenticationOptions = function (_a) {
567
- var token = _a.token;
555
+ var token = _a.token, challengeId = _a.challengeId;
568
556
  return __awaiter(this, void 0, void 0, function () {
569
- var response;
557
+ var body, response;
570
558
  return __generator(this, function (_b) {
571
559
  switch (_b.label) {
572
560
  case 0:
561
+ body = { challengeId: challengeId };
573
562
  response = fetch("".concat(this.baseUrl, "/client/user-authenticators/passkey/authentication-options"), {
574
563
  method: "POST",
575
564
  headers: this.buildHeaders(token),
576
- body: JSON.stringify({})
565
+ body: JSON.stringify(body)
577
566
  });
578
567
  return [4 /*yield*/, response];
579
568
  case 1: return [2 /*return*/, (_b.sent()).json()];
@@ -582,16 +571,20 @@ var PasskeyApiClient = /** @class */ (function () {
582
571
  });
583
572
  };
584
573
  PasskeyApiClient.prototype.addAuthenticator = function (_a) {
585
- var token = _a.token, rest = __rest(_a, ["token"]);
574
+ var token = _a.token, challengeId = _a.challengeId, registrationCredential = _a.registrationCredential;
586
575
  return __awaiter(this, void 0, void 0, function () {
587
- var response;
576
+ var body, response;
588
577
  return __generator(this, function (_b) {
589
578
  switch (_b.label) {
590
579
  case 0:
580
+ body = {
581
+ challengeId: challengeId,
582
+ registrationCredential: registrationCredential
583
+ };
591
584
  response = fetch("".concat(this.baseUrl, "/client/user-authenticators/passkey"), {
592
585
  method: "POST",
593
586
  headers: this.buildHeaders(token),
594
- body: JSON.stringify(rest)
587
+ body: JSON.stringify(body)
595
588
  });
596
589
  return [4 /*yield*/, response];
597
590
  case 1: return [2 /*return*/, (_b.sent()).json()];
@@ -600,16 +593,17 @@ var PasskeyApiClient = /** @class */ (function () {
600
593
  });
601
594
  };
602
595
  PasskeyApiClient.prototype.verify = function (_a) {
603
- var token = _a.token, rest = __rest(_a, ["token"]);
596
+ var token = _a.token, challengeId = _a.challengeId, authenticationCredential = _a.authenticationCredential, deviceId = _a.deviceId;
604
597
  return __awaiter(this, void 0, void 0, function () {
605
- var response;
598
+ var body, response;
606
599
  return __generator(this, function (_b) {
607
600
  switch (_b.label) {
608
601
  case 0:
602
+ body = { challengeId: challengeId, authenticationCredential: authenticationCredential, deviceId: deviceId };
609
603
  response = fetch("".concat(this.baseUrl, "/client/verify/passkey"), {
610
604
  method: "POST",
611
605
  headers: this.buildHeaders(token),
612
- body: JSON.stringify(rest)
606
+ body: JSON.stringify(body)
613
607
  });
614
608
  return [4 /*yield*/, response];
615
609
  case 1: return [2 /*return*/, (_b.sent()).json()];
@@ -636,6 +630,23 @@ var PasskeyApiClient = /** @class */ (function () {
636
630
  });
637
631
  });
638
632
  };
633
+ PasskeyApiClient.prototype.challenge = function (action) {
634
+ return __awaiter(this, void 0, void 0, function () {
635
+ var response;
636
+ return __generator(this, function (_a) {
637
+ switch (_a.label) {
638
+ case 0:
639
+ response = fetch("".concat(this.baseUrl, "/client/challenge"), {
640
+ method: "POST",
641
+ headers: this.buildHeaders(),
642
+ body: JSON.stringify({ action: action })
643
+ });
644
+ return [4 /*yield*/, response];
645
+ case 1: return [2 /*return*/, (_a.sent()).json()];
646
+ }
647
+ });
648
+ });
649
+ };
639
650
  PasskeyApiClient.prototype.buildHeaders = function (token) {
640
651
  var authorizationHeader = token ? "Bearer ".concat(token) : "Basic ".concat(window.btoa(encodeURIComponent(this.tenantId)));
641
652
  return {
@@ -648,9 +659,10 @@ var PasskeyApiClient = /** @class */ (function () {
648
659
 
649
660
  var Passkey = /** @class */ (function () {
650
661
  function Passkey(_a) {
651
- var baseUrl = _a.baseUrl, tenantId = _a.tenantId;
662
+ var baseUrl = _a.baseUrl, tenantId = _a.tenantId, anonymousId = _a.anonymousId;
652
663
  this.passkeyLocalStorageKey = "as_passkey_credential_id";
653
664
  this.api = new PasskeyApiClient({ baseUrl: baseUrl, tenantId: tenantId });
665
+ this.anonymousId = anonymousId;
654
666
  }
655
667
  Passkey.prototype.signUp = function (_a) {
656
668
  var userName = _a.userName, token = _a.token, _b = _a.authenticatorAttachment, authenticatorAttachment = _b === void 0 ? "platform" : _b;
@@ -658,7 +670,7 @@ var Passkey = /** @class */ (function () {
658
670
  var optionsResponse, registrationResponse, addAuthenticatorResponse;
659
671
  return __generator(this, function (_c) {
660
672
  switch (_c.label) {
661
- case 0: return [4 /*yield*/, this.api.registrationOptions({ userName: userName, token: token, authenticatorAttachment: authenticatorAttachment })];
673
+ case 0: return [4 /*yield*/, this.api.registrationOptions({ username: userName, token: token, authenticatorAttachment: authenticatorAttachment })];
662
674
  case 1:
663
675
  optionsResponse = _c.sent();
664
676
  return [4 /*yield*/, startRegistration(optionsResponse.options)];
@@ -681,26 +693,43 @@ var Passkey = /** @class */ (function () {
681
693
  };
682
694
  Passkey.prototype.signIn = function (params) {
683
695
  return __awaiter(this, void 0, void 0, function () {
684
- var optionsResponse, authenticationResponse, verifyResponse;
685
- return __generator(this, function (_a) {
686
- switch (_a.label) {
696
+ var challengeResponse, _a, optionsResponse, authenticationResponse, verifyResponse;
697
+ return __generator(this, function (_b) {
698
+ switch (_b.label) {
687
699
  case 0:
688
700
  if ((params === null || params === void 0 ? void 0 : params.token) && params.autofill) {
689
- throw new Error("Autofill is not supported when providing a token");
701
+ throw new Error("autofill is not supported when providing a token");
702
+ }
703
+ if ((params === null || params === void 0 ? void 0 : params.action) && params.token) {
704
+ throw new Error("action is not supported when providing a token");
690
705
  }
691
- return [4 /*yield*/, this.api.authenticationOptions({ token: params === null || params === void 0 ? void 0 : params.token })];
706
+ if (!(params === null || params === void 0 ? void 0 : params.action)) return [3 /*break*/, 2];
707
+ return [4 /*yield*/, this.api.challenge(params.action)];
692
708
  case 1:
693
- optionsResponse = _a.sent();
694
- return [4 /*yield*/, startAuthentication(optionsResponse.options, params === null || params === void 0 ? void 0 : params.autofill)];
709
+ _a = _b.sent();
710
+ return [3 /*break*/, 3];
695
711
  case 2:
696
- authenticationResponse = _a.sent();
712
+ _a = null;
713
+ _b.label = 3;
714
+ case 3:
715
+ challengeResponse = _a;
716
+ return [4 /*yield*/, this.api.authenticationOptions({
717
+ token: params === null || params === void 0 ? void 0 : params.token,
718
+ challengeId: challengeResponse === null || challengeResponse === void 0 ? void 0 : challengeResponse.challengeId
719
+ })];
720
+ case 4:
721
+ optionsResponse = _b.sent();
722
+ return [4 /*yield*/, startAuthentication(optionsResponse.options, params === null || params === void 0 ? void 0 : params.autofill)];
723
+ case 5:
724
+ authenticationResponse = _b.sent();
697
725
  return [4 /*yield*/, this.api.verify({
698
726
  challengeId: optionsResponse.challengeId,
699
727
  authenticationCredential: authenticationResponse,
700
- token: params === null || params === void 0 ? void 0 : params.token
728
+ token: params === null || params === void 0 ? void 0 : params.token,
729
+ deviceId: this.anonymousId
701
730
  })];
702
- case 3:
703
- verifyResponse = _a.sent();
731
+ case 6:
732
+ verifyResponse = _b.sent();
704
733
  if (verifyResponse === null || verifyResponse === void 0 ? void 0 : verifyResponse.isVerified) {
705
734
  this.storeCredentialAgainstDevice(authenticationResponse);
706
735
  }
@@ -777,408 +806,423 @@ function openWindow(_a) {
777
806
  return window.open(url, "", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=".concat(width, ", height=").concat(height, ", top=").concat(y, ", left=").concat(x));
778
807
  }
779
808
 
809
+ const not = {
810
+ inert: ':not([inert]):not([inert] *)',
811
+ negTabIndex: ':not([tabindex^="-"])',
812
+ disabled: ':not(:disabled)',
813
+ };
814
+
780
815
  var focusableSelectors = [
781
- 'a[href]:not([tabindex^="-"])',
782
- 'area[href]:not([tabindex^="-"])',
783
- 'input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])',
784
- 'input[type="radio"]:not([disabled]):not([tabindex^="-"])',
785
- 'select:not([disabled]):not([tabindex^="-"])',
786
- 'textarea:not([disabled]):not([tabindex^="-"])',
787
- 'button:not([disabled]):not([tabindex^="-"])',
788
- 'iframe:not([tabindex^="-"])',
789
- 'audio[controls]:not([tabindex^="-"])',
790
- 'video[controls]:not([tabindex^="-"])',
791
- '[contenteditable]:not([tabindex^="-"])',
792
- '[tabindex]:not([tabindex^="-"])',
816
+ `a[href]${not.inert}${not.negTabIndex}`,
817
+ `area[href]${not.inert}${not.negTabIndex}`,
818
+ `input:not([type="hidden"]):not([type="radio"])${not.inert}${not.negTabIndex}${not.disabled}`,
819
+ `input[type="radio"]${not.inert}${not.negTabIndex}${not.disabled}`,
820
+ `select${not.inert}${not.negTabIndex}${not.disabled}`,
821
+ `textarea${not.inert}${not.negTabIndex}${not.disabled}`,
822
+ `button${not.inert}${not.negTabIndex}${not.disabled}`,
823
+ `details${not.inert} > summary:first-of-type${not.negTabIndex}`,
824
+ // Discard until Firefox supports `:has()`
825
+ // See: https://github.com/KittyGiraudel/focusable-selectors/issues/12
826
+ // `details:not(:has(> summary))${not.inert}${not.negTabIndex}`,
827
+ `iframe${not.inert}${not.negTabIndex}`,
828
+ `audio[controls]${not.inert}${not.negTabIndex}`,
829
+ `video[controls]${not.inert}${not.negTabIndex}`,
830
+ `[contenteditable]${not.inert}${not.negTabIndex}`,
831
+ `[tabindex]${not.inert}${not.negTabIndex}`,
793
832
  ];
794
833
 
795
- var TAB_KEY = 'Tab';
796
- var ESCAPE_KEY = 'Escape';
797
-
798
834
  /**
799
- * Define the constructor to instantiate a dialog
800
- *
801
- * @constructor
802
- * @param {Element} element
835
+ * Set the focus to the first element with `autofocus` with the element or the
836
+ * element itself.
803
837
  */
804
- function A11yDialog(element) {
805
- // Prebind the functions that will be bound in addEventListener and
806
- // removeEventListener to avoid losing references
807
- this._show = this.show.bind(this);
808
- this._hide = this.hide.bind(this);
809
- this._maintainFocus = this._maintainFocus.bind(this);
810
- this._bindKeypress = this._bindKeypress.bind(this);
811
-
812
- this.$el = element;
813
- this.shown = false;
814
- this._id = this.$el.getAttribute('data-a11y-dialog') || this.$el.id;
815
- this._previouslyFocused = null;
816
- this._listeners = {};
817
-
818
- // Initialise everything needed for the dialog to work properly
819
- this.create();
838
+ function moveFocusToDialog(el) {
839
+ const focused = (el.querySelector('[autofocus]') || el);
840
+ focused.focus();
820
841
  }
821
-
822
- /**
823
- * Set up everything necessary for the dialog to be functioning
824
- *
825
- * @param {(NodeList | Element | string)} targets
826
- * @return {this}
827
- */
828
- A11yDialog.prototype.create = function () {
829
- this.$el.setAttribute('aria-hidden', true);
830
- this.$el.setAttribute('aria-modal', true);
831
- this.$el.setAttribute('tabindex', -1);
832
-
833
- if (!this.$el.hasAttribute('role')) {
834
- this.$el.setAttribute('role', 'dialog');
835
- }
836
-
837
- // Keep a collection of dialog openers, each of which will be bound a click
838
- // event listener to open the dialog
839
- this._openers = $$('[data-a11y-dialog-show="' + this._id + '"]');
840
- this._openers.forEach(
841
- function (opener) {
842
- opener.addEventListener('click', this._show);
843
- }.bind(this)
844
- );
845
-
846
- // Keep a collection of dialog closers, each of which will be bound a click
847
- // event listener to close the dialog
848
- const $el = this.$el;
849
-
850
- this._closers = $$('[data-a11y-dialog-hide]', this.$el)
851
- // This filter is necessary in case there are nested dialogs, so that
852
- // only closers from the current dialog are retrieved and effective
853
- .filter(function (closer) {
854
- // Testing for `[aria-modal="true"]` is not enough since this attribute
855
- // and the collect of closers is done at instantation time, when nested
856
- // dialogs might not have yet been instantiated. Note that if the dialogs
857
- // are manually instantiated, this could still fail because none of these
858
- // selectors would match; this would cause closers to close all parent
859
- // dialogs instead of just the current one
860
- return closer.closest('[aria-modal="true"], [data-a11y-dialog]') === $el
861
- })
862
- .concat($$('[data-a11y-dialog-hide="' + this._id + '"]'));
863
-
864
- this._closers.forEach(
865
- function (closer) {
866
- closer.addEventListener('click', this._hide);
867
- }.bind(this)
868
- );
869
-
870
- // Execute all callbacks registered for the `create` event
871
- this._fire('create');
872
-
873
- return this
874
- };
875
-
876
- /**
877
- * Show the dialog element, disable all the targets (siblings), trap the
878
- * current focus within it, listen for some specific key presses and fire all
879
- * registered callbacks for `show` event
880
- *
881
- * @param {CustomEvent} event
882
- * @return {this}
883
- */
884
- A11yDialog.prototype.show = function (event) {
885
- // If the dialog is already open, abort
886
- if (this.shown) {
887
- return this
888
- }
889
-
890
- // Keep a reference to the currently focused element to be able to restore
891
- // it later
892
- this._previouslyFocused = document.activeElement;
893
- this.$el.removeAttribute('aria-hidden');
894
- this.shown = true;
895
-
896
- // Set the focus to the dialog element
897
- moveFocusToDialog(this.$el);
898
-
899
- // Bind a focus event listener to the body element to make sure the focus
900
- // stays trapped inside the dialog while open, and start listening for some
901
- // specific key presses (TAB and ESC)
902
- document.body.addEventListener('focus', this._maintainFocus, true);
903
- document.addEventListener('keydown', this._bindKeypress);
904
-
905
- // Execute all callbacks registered for the `show` event
906
- this._fire('show', event);
907
-
908
- return this
909
- };
910
-
911
842
  /**
912
- * Hide the dialog element, enable all the targets (siblings), restore the
913
- * focus to the previously active element, stop listening for some specific
914
- * key presses and fire all registered callbacks for `hide` event
915
- *
916
- * @param {CustomEvent} event
917
- * @return {this}
843
+ * Get the first and last focusable elements in a given tree.
918
844
  */
919
- A11yDialog.prototype.hide = function (event) {
920
- // If the dialog is already closed, abort
921
- if (!this.shown) {
922
- return this
923
- }
924
-
925
- this.shown = false;
926
- this.$el.setAttribute('aria-hidden', 'true');
927
-
928
- // If there was a focused element before the dialog was opened (and it has a
929
- // `focus` method), restore the focus back to it
930
- // See: https://github.com/KittyGiraudel/a11y-dialog/issues/108
931
- if (this._previouslyFocused && this._previouslyFocused.focus) {
932
- this._previouslyFocused.focus();
933
- }
934
-
935
- // Remove the focus event listener to the body element and stop listening
936
- // for specific key presses
937
- document.body.removeEventListener('focus', this._maintainFocus, true);
938
- document.removeEventListener('keydown', this._bindKeypress);
939
-
940
- // Execute all callbacks registered for the `hide` event
941
- this._fire('hide', event);
942
-
943
- return this
944
- };
945
-
946
- /**
947
- * Destroy the current instance (after making sure the dialog has been hidden)
948
- * and remove all associated listeners from dialog openers and closers
949
- *
950
- * @return {this}
951
- */
952
- A11yDialog.prototype.destroy = function () {
953
- // Hide the dialog to avoid destroying an open instance
954
- this.hide();
955
-
956
- // Remove the click event listener from all dialog openers
957
- this._openers.forEach(
958
- function (opener) {
959
- opener.removeEventListener('click', this._show);
960
- }.bind(this)
961
- );
962
-
963
- // Remove the click event listener from all dialog closers
964
- this._closers.forEach(
965
- function (closer) {
966
- closer.removeEventListener('click', this._hide);
967
- }.bind(this)
968
- );
969
-
970
- // Execute all callbacks registered for the `destroy` event
971
- this._fire('destroy');
972
-
973
- // Keep an object of listener types mapped to callback functions
974
- this._listeners = {};
975
-
976
- return this
977
- };
978
-
979
- /**
980
- * Register a new callback for the given event type
981
- *
982
- * @param {string} type
983
- * @param {Function} handler
984
- */
985
- A11yDialog.prototype.on = function (type, handler) {
986
- if (typeof this._listeners[type] === 'undefined') {
987
- this._listeners[type] = [];
988
- }
989
-
990
- this._listeners[type].push(handler);
991
-
992
- return this
993
- };
994
-
995
- /**
996
- * Unregister an existing callback for the given event type
997
- *
998
- * @param {string} type
999
- * @param {Function} handler
1000
- */
1001
- A11yDialog.prototype.off = function (type, handler) {
1002
- var index = (this._listeners[type] || []).indexOf(handler);
1003
-
1004
- if (index > -1) {
1005
- this._listeners[type].splice(index, 1);
1006
- }
1007
-
1008
- return this
1009
- };
1010
-
845
+ function getFocusableEdges(el) {
846
+ // Check for a focusable element within the subtree of `el`.
847
+ const first = findFocusableElement(el, true);
848
+ // Only if we find the first element do we need to look for the last one. If
849
+ // there’s no last element, we set `last` as a reference to `first` so that
850
+ // the returned array is always of length 2.
851
+ const last = first ? findFocusableElement(el, false) || first : null;
852
+ return [first, last];
853
+ }
1011
854
  /**
1012
- * Iterate over all registered handlers for given type and call them all with
1013
- * the dialog element as first argument, event as second argument (if any). Also
1014
- * dispatch a custom event on the DOM element itself to make it possible to
1015
- * react to the lifecycle of auto-instantiated dialogs.
1016
- *
1017
- * @access private
1018
- * @param {string} type
1019
- * @param {CustomEvent} event
855
+ * Find the first focusable element inside the given node if `forward` is truthy
856
+ * or the last focusable element otherwise.
1020
857
  */
1021
- A11yDialog.prototype._fire = function (type, event) {
1022
- var listeners = this._listeners[type] || [];
1023
- var domEvent = new CustomEvent(type, { detail: event });
1024
-
1025
- this.$el.dispatchEvent(domEvent);
1026
-
1027
- listeners.forEach(
1028
- function (listener) {
1029
- listener(this.$el, event);
1030
- }.bind(this)
1031
- );
1032
- };
1033
-
858
+ function findFocusableElement(node, forward) {
859
+ // If we’re walking forward, check if this node is focusable, and return it
860
+ // immediately if it is.
861
+ if (forward && isFocusable(node))
862
+ return node;
863
+ // We should only search the subtree of this node if it can have focusable
864
+ // children.
865
+ if (canHaveFocusableChildren(node)) {
866
+ // Start walking the DOM tree, looking for focusable elements.
867
+ // Case 1: If this node has a shadow root, search it recursively.
868
+ if (node.shadowRoot) {
869
+ // Descend into this subtree.
870
+ let next = getNextChildEl(node.shadowRoot, forward);
871
+ // Traverse siblings, searching the subtree of each one
872
+ // for focusable elements.
873
+ while (next) {
874
+ const focusableEl = findFocusableElement(next, forward);
875
+ if (focusableEl)
876
+ return focusableEl;
877
+ next = getNextSiblingEl(next, forward);
878
+ }
879
+ }
880
+ // Case 2: If this node is a slot for a Custom Element, search its assigned
881
+ // nodes recursively.
882
+ else if (node.localName === 'slot') {
883
+ const assignedElements = node.assignedElements({
884
+ flatten: true,
885
+ });
886
+ if (!forward)
887
+ assignedElements.reverse();
888
+ for (const assignedElement of assignedElements) {
889
+ const focusableEl = findFocusableElement(assignedElement, forward);
890
+ if (focusableEl)
891
+ return focusableEl;
892
+ }
893
+ }
894
+ // Case 3: this is a regular Light DOM node. Search its subtree.
895
+ else {
896
+ // Descend into this subtree.
897
+ let next = getNextChildEl(node, forward);
898
+ // Traverse siblings, searching the subtree of each one
899
+ // for focusable elements.
900
+ while (next) {
901
+ const focusableEl = findFocusableElement(next, forward);
902
+ if (focusableEl)
903
+ return focusableEl;
904
+ next = getNextSiblingEl(next, forward);
905
+ }
906
+ }
907
+ }
908
+ // If we’re walking backward, we want to check the node’s entire subtree
909
+ // before checking the node itself. If this node is focusable, return it.
910
+ if (!forward && isFocusable(node))
911
+ return node;
912
+ return null;
913
+ }
914
+ function getNextChildEl(node, forward) {
915
+ return forward ? node.firstElementChild : node.lastElementChild;
916
+ }
917
+ function getNextSiblingEl(el, forward) {
918
+ return forward ? el.nextElementSibling : el.previousElementSibling;
919
+ }
1034
920
  /**
1035
- * Private event handler used when listening to some specific key presses
1036
- * (namely ESCAPE and TAB)
1037
- *
1038
- * @access private
1039
- * @param {Event} event
921
+ * Determine if an element is hidden from the user.
1040
922
  */
1041
- A11yDialog.prototype._bindKeypress = function (event) {
1042
- // This is an escape hatch in case there are nested dialogs, so the keypresses
1043
- // are only reacted to for the most recent one
1044
- const focused = document.activeElement;
1045
- if (focused && focused.closest('[aria-modal="true"]') !== this.$el) return
1046
-
1047
- // If the dialog is shown and the ESCAPE key is being pressed, prevent any
1048
- // further effects from the ESCAPE key and hide the dialog, unless its role
1049
- // is 'alertdialog', which should be modal
1050
- if (
1051
- this.shown &&
1052
- event.key === ESCAPE_KEY &&
1053
- this.$el.getAttribute('role') !== 'alertdialog'
1054
- ) {
1055
- event.preventDefault();
1056
- this.hide(event);
1057
- }
1058
-
1059
- // If the dialog is shown and the TAB key is being pressed, make sure the
1060
- // focus stays trapped within the dialog element
1061
- if (this.shown && event.key === TAB_KEY) {
1062
- trapTabKey(this.$el, event);
1063
- }
923
+ const isHidden = (el) => {
924
+ // Browsers hide all non-<summary> descendants of closed <details> elements
925
+ // from user interaction, but those non-<summary> elements may still match our
926
+ // focusable-selectors and may still have dimensions, so we need a special
927
+ // case to ignore them.
928
+ if (el.matches('details:not([open]) *') &&
929
+ !el.matches('details>summary:first-of-type'))
930
+ return true;
931
+ // If this element has no painted dimensions, it's hidden.
932
+ return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
1064
933
  };
1065
-
1066
934
  /**
1067
- * Private event handler used when making sure the focus stays within the
1068
- * currently open dialog
1069
- *
1070
- * @access private
1071
- * @param {Event} event
935
+ * Determine if an element is focusable and has user-visible painted dimensions.
1072
936
  */
1073
- A11yDialog.prototype._maintainFocus = function (event) {
1074
- // If the dialog is shown and the focus is not within a dialog element (either
1075
- // this one or another one in case of nested dialogs) or within an element
1076
- // with the `data-a11y-dialog-focus-trap-ignore` attribute, move it back to
1077
- // its first focusable child.
1078
- // See: https://github.com/KittyGiraudel/a11y-dialog/issues/177
1079
- if (
1080
- this.shown &&
1081
- !event.target.closest('[aria-modal="true"]') &&
1082
- !event.target.closest('[data-a11y-dialog-ignore-focus-trap]')
1083
- ) {
1084
- moveFocusToDialog(this.$el);
1085
- }
937
+ const isFocusable = (el) => {
938
+ // A shadow host that delegates focus will never directly receive focus,
939
+ // even with `tabindex=0`. Consider our <fancy-button> custom element, which
940
+ // delegates focus to its shadow button:
941
+ //
942
+ // <fancy-button tabindex="0">
943
+ // #shadow-root
944
+ // <button><slot></slot></button>
945
+ // </fancy-button>
946
+ //
947
+ // The browser acts as as if there is only one focusable element – the shadow
948
+ // button. Our library should behave the same way.
949
+ if (el.shadowRoot?.delegatesFocus)
950
+ return false;
951
+ return el.matches(focusableSelectors.join(',')) && !isHidden(el);
1086
952
  };
1087
-
1088
- /**
1089
- * Convert a NodeList into an array
1090
- *
1091
- * @param {NodeList} collection
1092
- * @return {Array<Element>}
1093
- */
1094
- function toArray(collection) {
1095
- return Array.prototype.slice.call(collection)
1096
- }
1097
-
1098
953
  /**
1099
- * Query the DOM for nodes matching the given selector, scoped to context (or
1100
- * the whole document)
1101
- *
1102
- * @param {String} selector
1103
- * @param {Element} [context = document]
1104
- * @return {Array<Element>}
954
+ * Determine if an element can have focusable children. Useful for bailing out
955
+ * early when walking the DOM tree.
956
+ * @example
957
+ * This div is inert, so none of its children can be focused, even though they
958
+ * meet our criteria for what is focusable. Once we check the div, we can skip
959
+ * the rest of the subtree.
960
+ * ```html
961
+ * <div inert>
962
+ * <button>Button</button>
963
+ * <a href="#">Link</a>
964
+ * </div>
965
+ * ```
1105
966
  */
1106
- function $$(selector, context) {
1107
- return toArray((context || document).querySelectorAll(selector))
967
+ function canHaveFocusableChildren(el) {
968
+ // The browser will never send focus into a Shadow DOM if the host element
969
+ // has a negative tabindex. This applies to both slotted Light DOM Shadow DOM
970
+ // children
971
+ if (el.shadowRoot && el.getAttribute('tabindex') === '-1')
972
+ return false;
973
+ // Elemments matching this selector are either hidden entirely from the user,
974
+ // or are visible but unavailable for interaction. Their descentants can never
975
+ // receive focus.
976
+ return !el.matches(':disabled,[hidden],[inert]');
1108
977
  }
1109
-
1110
978
  /**
1111
- * Set the focus to the first element with `autofocus` with the element or the
1112
- * element itself
1113
- *
1114
- * @param {Element} node
979
+ * Get the active element, accounting for Shadow DOM subtrees.
980
+ * @author Cory LaViska
981
+ * @see: https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
1115
982
  */
1116
- function moveFocusToDialog(node) {
1117
- var focused = node.querySelector('[autofocus]') || node;
1118
-
1119
- focused.focus();
1120
- }
1121
-
1122
- /**
1123
- * Get the focusable children of the given element
1124
- *
1125
- * @param {Element} node
1126
- * @return {Array<Element>}
1127
- */
1128
- function getFocusableChildren(node) {
1129
- return $$(focusableSelectors.join(','), node).filter(function (child) {
1130
- return !!(
1131
- child.offsetWidth ||
1132
- child.offsetHeight ||
1133
- child.getClientRects().length
1134
- )
1135
- })
983
+ function getActiveElement(root = document) {
984
+ const activeEl = root.activeElement;
985
+ if (!activeEl)
986
+ return null;
987
+ // If there’s a shadow root, recursively find the active element within it.
988
+ // If the recursive call returns null, return the active element
989
+ // of the top-level Document.
990
+ if (activeEl.shadowRoot)
991
+ return getActiveElement(activeEl.shadowRoot) || document.activeElement;
992
+ // If not, we can just return the active element
993
+ return activeEl;
1136
994
  }
1137
-
1138
995
  /**
1139
996
  * Trap the focus inside the given element
1140
- *
1141
- * @param {Element} node
1142
- * @param {Event} event
1143
997
  */
1144
- function trapTabKey(node, event) {
1145
- var focusableChildren = getFocusableChildren(node);
1146
- var focusedItemIndex = focusableChildren.indexOf(document.activeElement);
998
+ function trapTabKey(el, event) {
999
+ const [firstFocusableChild, lastFocusableChild] = getFocusableEdges(el);
1000
+ // If there are no focusable children in the dialog, prevent the user from
1001
+ // tabbing out of it
1002
+ if (!firstFocusableChild)
1003
+ return event.preventDefault();
1004
+ const activeElement = getActiveElement();
1005
+ // If the SHIFT key is pressed while tabbing (moving backwards) and the
1006
+ // currently focused item is the first one, move the focus to the last
1007
+ // focusable item from the dialog element
1008
+ if (event.shiftKey && activeElement === firstFocusableChild) {
1009
+ // @ts-ignore: we know that `lastFocusableChild` is not null here
1010
+ lastFocusableChild.focus();
1011
+ event.preventDefault();
1012
+ }
1013
+ // If the SHIFT key is not pressed (moving forwards) and the currently focused
1014
+ // item is the last one, move the focus to the first focusable item from the
1015
+ // dialog element
1016
+ else if (!event.shiftKey && activeElement === lastFocusableChild) {
1017
+ firstFocusableChild.focus();
1018
+ event.preventDefault();
1019
+ }
1020
+ }
1147
1021
 
1148
- // If the SHIFT key is being pressed while tabbing (moving backwards) and
1149
- // the currently focused item is the first one, move the focus to the last
1150
- // focusable item from the dialog element
1151
- if (event.shiftKey && focusedItemIndex === 0) {
1152
- focusableChildren[focusableChildren.length - 1].focus();
1153
- event.preventDefault();
1154
- // If the SHIFT key is not being pressed (moving forwards) and the currently
1155
- // focused item is the last one, move the focus to the first focusable item
1156
- // from the dialog element
1157
- } else if (
1158
- !event.shiftKey &&
1159
- focusedItemIndex === focusableChildren.length - 1
1160
- ) {
1161
- focusableChildren[0].focus();
1162
- event.preventDefault();
1163
- }
1022
+ class A11yDialog {
1023
+ $el;
1024
+ id;
1025
+ previouslyFocused;
1026
+ shown;
1027
+ constructor(element) {
1028
+ this.$el = element;
1029
+ this.id = this.$el.getAttribute('data-a11y-dialog') || this.$el.id;
1030
+ this.previouslyFocused = null;
1031
+ this.shown = false;
1032
+ this.maintainFocus = this.maintainFocus.bind(this);
1033
+ this.bindKeypress = this.bindKeypress.bind(this);
1034
+ this.handleTriggerClicks = this.handleTriggerClicks.bind(this);
1035
+ this.show = this.show.bind(this);
1036
+ this.hide = this.hide.bind(this);
1037
+ this.$el.setAttribute('aria-hidden', 'true');
1038
+ this.$el.setAttribute('aria-modal', 'true');
1039
+ this.$el.setAttribute('tabindex', '-1');
1040
+ if (!this.$el.hasAttribute('role')) {
1041
+ this.$el.setAttribute('role', 'dialog');
1042
+ }
1043
+ document.addEventListener('click', this.handleTriggerClicks, true);
1044
+ }
1045
+ /**
1046
+ * Destroy the current instance (after making sure the dialog has been hidden)
1047
+ * and remove all associated listeners from dialog openers and closers
1048
+ */
1049
+ destroy() {
1050
+ // Hide the dialog to avoid destroying an open instance
1051
+ this.hide();
1052
+ // Remove the click event delegates for our openers and closers
1053
+ document.removeEventListener('click', this.handleTriggerClicks, true);
1054
+ // Clone and replace the dialog element to prevent memory leaks caused by
1055
+ // event listeners that the author might not have cleaned up.
1056
+ this.$el.replaceWith(this.$el.cloneNode(true));
1057
+ // Dispatch a `destroy` event
1058
+ this.fire('destroy');
1059
+ return this;
1060
+ }
1061
+ /**
1062
+ * Show the dialog element, trap the current focus within it, listen for some
1063
+ * specific key presses and fire all registered callbacks for `show` event
1064
+ */
1065
+ show(event) {
1066
+ // If the dialog is already open, abort
1067
+ if (this.shown)
1068
+ return this;
1069
+ // Keep a reference to the currently focused element to be able to restore
1070
+ // it later
1071
+ this.shown = true;
1072
+ this.$el.removeAttribute('aria-hidden');
1073
+ this.previouslyFocused = getActiveElement();
1074
+ // Due to a long lasting bug in Safari, clicking an interactive element
1075
+ // (like a <button>) does *not* move the focus to that element, which means
1076
+ // `document.activeElement` is whatever element is currently focused (like
1077
+ // an <input>), or the <body> element otherwise. We can work around that
1078
+ // problem by checking whether the focused element is the <body>, and if it,
1079
+ // store the click event target.
1080
+ // See: https://bugs.webkit.org/show_bug.cgi?id=22261
1081
+ if (this.previouslyFocused?.tagName === 'BODY' && event?.target) {
1082
+ this.previouslyFocused = event.target;
1083
+ }
1084
+ // Set the focus to the dialog element
1085
+ // See: https://github.com/KittyGiraudel/a11y-dialog/pull/583
1086
+ if (event?.type === 'focus') {
1087
+ this.maintainFocus(event);
1088
+ }
1089
+ else {
1090
+ moveFocusToDialog(this.$el);
1091
+ }
1092
+ // Bind a focus event listener to the body element to make sure the focus
1093
+ // stays trapped inside the dialog while open, and start listening for some
1094
+ // specific key presses (TAB and ESC)
1095
+ document.body.addEventListener('focus', this.maintainFocus, true);
1096
+ this.$el.addEventListener('keydown', this.bindKeypress, true);
1097
+ // Dispatch a `show` event
1098
+ this.fire('show', event);
1099
+ return this;
1100
+ }
1101
+ /**
1102
+ * Hide the dialog element, restore the focus to the previously active
1103
+ * element, stop listening for some specific key presses and fire all
1104
+ * registered callbacks for `hide` event
1105
+ */
1106
+ hide(event) {
1107
+ // If the dialog is already closed, abort
1108
+ if (!this.shown)
1109
+ return this;
1110
+ this.shown = false;
1111
+ this.$el.setAttribute('aria-hidden', 'true');
1112
+ this.previouslyFocused?.focus?.();
1113
+ // Remove the focus event listener to the body element and stop listening
1114
+ // for specific key presses
1115
+ document.body.removeEventListener('focus', this.maintainFocus, true);
1116
+ this.$el.removeEventListener('keydown', this.bindKeypress, true);
1117
+ // Dispatch a `hide` event
1118
+ this.fire('hide', event);
1119
+ return this;
1120
+ }
1121
+ /**
1122
+ * Register a new callback for the given event type
1123
+ */
1124
+ on(type, handler, options) {
1125
+ this.$el.addEventListener(type, handler, options);
1126
+ return this;
1127
+ }
1128
+ /**
1129
+ * Unregister an existing callback for the given event type
1130
+ */
1131
+ off(type, handler, options) {
1132
+ this.$el.removeEventListener(type, handler, options);
1133
+ return this;
1134
+ }
1135
+ /**
1136
+ * Dispatch a custom event from the DOM element associated with this dialog.
1137
+ * This allows authors to listen for and respond to the events in their own
1138
+ * code
1139
+ */
1140
+ fire(type, event) {
1141
+ this.$el.dispatchEvent(new CustomEvent(type, {
1142
+ detail: event,
1143
+ cancelable: true,
1144
+ }));
1145
+ }
1146
+ /**
1147
+ * Add a delegated event listener for when elememts that open or close the
1148
+ * dialog are clicked, and call `show` or `hide`, respectively
1149
+ */
1150
+ handleTriggerClicks(event) {
1151
+ const target = event.target;
1152
+ // We use `.closest(..)` and not `.matches(..)` here so that clicking
1153
+ // an element nested within a dialog opener does cause the dialog to open
1154
+ if (target.closest(`[data-a11y-dialog-show="${this.id}"]`)) {
1155
+ this.show(event);
1156
+ }
1157
+ if (target.closest(`[data-a11y-dialog-hide="${this.id}"]`) ||
1158
+ (target.closest('[data-a11y-dialog-hide]') &&
1159
+ target.closest('[aria-modal="true"]') === this.$el)) {
1160
+ this.hide(event);
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Private event handler used when listening to some specific key presses
1165
+ * (namely ESC and TAB)
1166
+ */
1167
+ bindKeypress(event) {
1168
+ // This is an escape hatch in case there are nested open dialogs, so that
1169
+ // only the top most dialog gets interacted with
1170
+ if (document.activeElement?.closest('[aria-modal="true"]') !== this.$el) {
1171
+ return;
1172
+ }
1173
+ let hasOpenPopover = false;
1174
+ try {
1175
+ hasOpenPopover = !!this.$el.querySelector('[popover]:not([popover="manual"]):popover-open');
1176
+ }
1177
+ catch {
1178
+ // Run that DOM query in a try/catch because not all browsers support the
1179
+ // `:popover-open` selector, which would cause the whole expression to
1180
+ // fail
1181
+ // See: https://caniuse.com/mdn-css_selectors_popover-open
1182
+ // See: https://github.com/KittyGiraudel/a11y-dialog/pull/578#discussion_r1343215149
1183
+ }
1184
+ // If the dialog is shown and the ESC key is pressed, prevent any further
1185
+ // effects from the ESC key and hide the dialog, unless:
1186
+ // - its role is `alertdialog`, which means it should be modal
1187
+ // - or it contains an open popover, in which case ESC should close it
1188
+ if (event.key === 'Escape' &&
1189
+ this.$el.getAttribute('role') !== 'alertdialog' &&
1190
+ !hasOpenPopover) {
1191
+ event.preventDefault();
1192
+ this.hide(event);
1193
+ }
1194
+ // If the dialog is shown and the TAB key is pressed, make sure the focus
1195
+ // stays trapped within the dialog element
1196
+ if (event.key === 'Tab') {
1197
+ trapTabKey(this.$el, event);
1198
+ }
1199
+ }
1200
+ /**
1201
+ * If the dialog is shown and the focus is not within a dialog element (either
1202
+ * this one or another one in case of nested dialogs) or attribute, move it
1203
+ * back to the dialog container
1204
+ * See: https://github.com/KittyGiraudel/a11y-dialog/issues/177
1205
+ */
1206
+ maintainFocus(event) {
1207
+ const target = event.target;
1208
+ if (!target.closest('[aria-modal="true"], [data-a11y-dialog-ignore-focus-trap]')) {
1209
+ moveFocusToDialog(this.$el);
1210
+ }
1211
+ }
1164
1212
  }
1165
1213
 
1166
1214
  function instantiateDialogs() {
1167
- $$('[data-a11y-dialog]').forEach(function (node) {
1168
- new A11yDialog(node);
1169
- });
1215
+ for (const el of document.querySelectorAll('[data-a11y-dialog]')) {
1216
+ new A11yDialog(el);
1217
+ }
1170
1218
  }
1171
-
1172
1219
  if (typeof document !== 'undefined') {
1173
- if (document.readyState === 'loading') {
1174
- document.addEventListener('DOMContentLoaded', instantiateDialogs);
1175
- } else {
1176
- if (window.requestAnimationFrame) {
1177
- window.requestAnimationFrame(instantiateDialogs);
1178
- } else {
1179
- window.setTimeout(instantiateDialogs, 16);
1220
+ if (document.readyState === 'loading') {
1221
+ document.addEventListener('DOMContentLoaded', instantiateDialogs);
1222
+ }
1223
+ else {
1224
+ instantiateDialogs();
1180
1225
  }
1181
- }
1182
1226
  }
1183
1227
 
1184
1228
  var CONTAINER_ID = "__authsignal-popup-container";
@@ -1190,16 +1234,16 @@ var DEFAULT_WIDTH = "385px";
1190
1234
  var INITIAL_HEIGHT = "384px";
1191
1235
  var PopupHandler = /** @class */ (function () {
1192
1236
  function PopupHandler(_a) {
1193
- var width = _a.width;
1237
+ var width = _a.width, isClosable = _a.isClosable;
1194
1238
  this.popup = null;
1195
1239
  if (document.querySelector("#".concat(CONTAINER_ID))) {
1196
1240
  throw new Error("Multiple instances of Authsignal popup is not supported.");
1197
1241
  }
1198
- this.create({ width: width });
1242
+ this.create({ width: width, isClosable: isClosable });
1199
1243
  }
1200
1244
  PopupHandler.prototype.create = function (_a) {
1201
1245
  var _this = this;
1202
- var _b = _a.width, width = _b === void 0 ? DEFAULT_WIDTH : _b;
1246
+ var _b = _a.width, width = _b === void 0 ? DEFAULT_WIDTH : _b, _c = _a.isClosable, isClosable = _c === void 0 ? true : _c;
1203
1247
  var isWidthValidCSSValue = CSS.supports("width", width);
1204
1248
  var popupWidth = width;
1205
1249
  if (!isWidthValidCSSValue) {
@@ -1210,10 +1254,15 @@ var PopupHandler = /** @class */ (function () {
1210
1254
  var container = document.createElement("div");
1211
1255
  container.setAttribute("id", CONTAINER_ID);
1212
1256
  container.setAttribute("aria-hidden", "true");
1257
+ if (!isClosable) {
1258
+ container.setAttribute("role", "alertdialog");
1259
+ }
1213
1260
  // Create dialog overlay
1214
1261
  var overlay = document.createElement("div");
1215
1262
  overlay.setAttribute("id", OVERLAY_ID);
1216
- overlay.setAttribute("data-a11y-dialog-hide", "true");
1263
+ if (isClosable) {
1264
+ overlay.setAttribute("data-a11y-dialog-hide", "true");
1265
+ }
1217
1266
  // Create dialog content
1218
1267
  var content = document.createElement("div");
1219
1268
  content.setAttribute("id", CONTENT_ID);
@@ -1267,11 +1316,11 @@ var PopupHandler = /** @class */ (function () {
1267
1316
  }
1268
1317
  this.popup.hide();
1269
1318
  };
1270
- PopupHandler.prototype.on = function (event, handler) {
1319
+ PopupHandler.prototype.on = function (event, listener) {
1271
1320
  if (!this.popup) {
1272
1321
  throw new Error("Popup is not initialized");
1273
1322
  }
1274
- this.popup.on(event, handler);
1323
+ this.popup.on(event, listener);
1275
1324
  };
1276
1325
  return PopupHandler;
1277
1326
  }());
@@ -1299,7 +1348,6 @@ var Authsignal = /** @class */ (function () {
1299
1348
  if (!tenantId) {
1300
1349
  throw new Error("tenantId is required");
1301
1350
  }
1302
- this.passkey = new Passkey({ tenantId: tenantId, baseUrl: baseUrl });
1303
1351
  var idCookie = getCookie(this.anonymousIdCookieName);
1304
1352
  if (idCookie) {
1305
1353
  this.anonymousId = idCookie;
@@ -1314,6 +1362,7 @@ var Authsignal = /** @class */ (function () {
1314
1362
  secure: document.location.protocol !== "http:"
1315
1363
  });
1316
1364
  }
1365
+ this.passkey = new Passkey({ tenantId: tenantId, baseUrl: baseUrl, anonymousId: this.anonymousId });
1317
1366
  }
1318
1367
  Authsignal.prototype.launch = function (url, options) {
1319
1368
  switch (options === null || options === void 0 ? void 0 : options.mode) {
@@ -1366,7 +1415,7 @@ var Authsignal = /** @class */ (function () {
1366
1415
  Authsignal.prototype.launchWithPopup = function (url, options) {
1367
1416
  var _this = this;
1368
1417
  var popupOptions = options.popupOptions;
1369
- var popupHandler = new PopupHandler({ width: popupOptions === null || popupOptions === void 0 ? void 0 : popupOptions.width });
1418
+ var popupHandler = new PopupHandler({ width: popupOptions === null || popupOptions === void 0 ? void 0 : popupOptions.width, isClosable: popupOptions === null || popupOptions === void 0 ? void 0 : popupOptions.isClosable });
1370
1419
  var popupUrl = "".concat(url, "&mode=popup");
1371
1420
  popupHandler.show({ url: popupUrl });
1372
1421
  return new Promise(function (resolve) {