@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/api/passkey-api-client.d.ts +14 -5
- package/dist/api/types.d.ts +6 -5
- package/dist/handlers/popup-handler.d.ts +5 -5
- package/dist/index.js +464 -415
- package/dist/index.min.js +1 -1
- package/dist/passkey.d.ts +7 -1
- package/dist/types.d.ts +4 -0
- package/package.json +2 -2
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,
|
|
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:
|
|
554
|
-
: { 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,
|
|
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(
|
|
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,
|
|
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(
|
|
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({
|
|
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 (
|
|
686
|
-
switch (
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
694
|
-
return [
|
|
709
|
+
_a = _b.sent();
|
|
710
|
+
return [3 /*break*/, 3];
|
|
695
711
|
case 2:
|
|
696
|
-
|
|
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
|
|
703
|
-
verifyResponse =
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
*
|
|
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
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
*
|
|
1013
|
-
* the
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
*
|
|
1100
|
-
* the
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
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
|
|
1107
|
-
|
|
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
|
-
*
|
|
1112
|
-
*
|
|
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
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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(
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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) {
|