@economist/web-apple-pay 1.7.9 → 1.7.10
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 +65 -79
- package/dist/src/apple-pay-button.js +65 -79
- package/dist/src/apple-pay-button.stories.js +65 -79
- package/dist/src/apple-pay-modal.d.ts +9 -14
- package/dist/src/apple-pay-modal.js +36 -44
- package/dist/src/apple-pay-modal.stories.js +65 -79
- package/dist/src/clients/payment-checkout/apple-session.d.ts +1 -0
- package/dist/src/clients/payment-checkout/apple-session.js +11 -6
- package/dist/src/index.js +65 -79
- package/dist/src/utils/recaptcha.d.ts +9 -3
- package/dist/src/utils/recaptcha.js +6 -5
- package/package.json +1 -1
|
@@ -578,10 +578,9 @@ var loadRecaptchaScript = (siteKey) => {
|
|
|
578
578
|
);
|
|
579
579
|
if (existingScript) {
|
|
580
580
|
existingScript.addEventListener("load", () => resolve());
|
|
581
|
-
existingScript.addEventListener(
|
|
582
|
-
"
|
|
583
|
-
|
|
584
|
-
);
|
|
581
|
+
existingScript.addEventListener("error", () => {
|
|
582
|
+
reject(new Error("Failed to load existing reCAPTCHA script"));
|
|
583
|
+
});
|
|
585
584
|
return;
|
|
586
585
|
}
|
|
587
586
|
const script = document.createElement("script");
|
|
@@ -589,7 +588,9 @@ var loadRecaptchaScript = (siteKey) => {
|
|
|
589
588
|
script.async = true;
|
|
590
589
|
script.defer = true;
|
|
591
590
|
script.onload = () => resolve();
|
|
592
|
-
script.onerror = () =>
|
|
591
|
+
script.onerror = () => {
|
|
592
|
+
reject(new Error("Failed to load reCAPTCHA script"));
|
|
593
|
+
};
|
|
593
594
|
document.head.appendChild(script);
|
|
594
595
|
});
|
|
595
596
|
};
|
|
@@ -838,17 +839,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
838
839
|
#country = "";
|
|
839
840
|
#triggerElement = null;
|
|
840
841
|
#keyHandler = null;
|
|
841
|
-
#
|
|
842
|
-
#recaptchaTokensPromise = null;
|
|
842
|
+
#isClosed = false;
|
|
843
843
|
/**
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* until the token pre-fetch resolves, so this callback is never reached
|
|
851
|
-
* without valid tokens.
|
|
844
|
+
* Callback invoked synchronously from the CTA click handler.
|
|
845
|
+
* Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
|
|
846
|
+
* by the time this is called. The caller (ApplePayButton) must call
|
|
847
|
+
* new ApplePaySession() synchronously inside this callback to stay within
|
|
848
|
+
* Safari's user-gesture window. The token promise resolves before
|
|
849
|
+
* onPaymentAuthorized fires (user still needs to authenticate on the sheet).
|
|
852
850
|
*/
|
|
853
851
|
onConfirm = null;
|
|
854
852
|
set offer(value) {
|
|
@@ -945,28 +943,32 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
945
943
|
}
|
|
946
944
|
});
|
|
947
945
|
}
|
|
948
|
-
|
|
949
|
-
if (cta)
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
946
|
+
cta?.addEventListener("click", () => {
|
|
947
|
+
if (cta.disabled) return;
|
|
948
|
+
if (isUS && checkbox && !checkbox.checked) {
|
|
949
|
+
errorElement?.classList.add("apm__error--visible");
|
|
950
|
+
checkboxLabel?.classList.add("apm__checkbox-label--error");
|
|
951
|
+
return;
|
|
953
952
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
953
|
+
errorElement?.classList.remove("apm__error--visible");
|
|
954
|
+
this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
|
|
955
|
+
cta.disabled = true;
|
|
956
|
+
cta.setAttribute("aria-busy", "true");
|
|
957
|
+
const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
|
|
958
|
+
if (!this.#isClosed && this.onConfirm) {
|
|
959
|
+
this.onConfirm(recaptchaTokensPromise);
|
|
960
|
+
this.close();
|
|
961
|
+
}
|
|
962
|
+
recaptchaTokensPromise.catch(() => {
|
|
963
|
+
if (this.#isClosed) return;
|
|
964
|
+
cta.disabled = false;
|
|
965
|
+
cta.removeAttribute("aria-busy");
|
|
965
966
|
const existingError = this.querySelector("#apple-pay-modal-error");
|
|
966
967
|
if (existingError) {
|
|
967
968
|
const icon = existingError.querySelector("svg");
|
|
968
969
|
existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
|
|
969
970
|
if (icon) existingError.prepend(icon);
|
|
971
|
+
existingError.setAttribute("role", "alert");
|
|
970
972
|
existingError.classList.add("apm__error--visible");
|
|
971
973
|
} else {
|
|
972
974
|
const errorNotice = document.createElement("div");
|
|
@@ -976,25 +978,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
976
978
|
cta?.insertAdjacentElement("beforebegin", errorNotice);
|
|
977
979
|
}
|
|
978
980
|
});
|
|
979
|
-
}
|
|
980
|
-
cta?.addEventListener("click", () => {
|
|
981
|
-
if (isUS && checkbox && !checkbox.checked) {
|
|
982
|
-
errorElement?.classList.add("apm__error--visible");
|
|
983
|
-
checkboxLabel?.classList.add("apm__checkbox-label--error");
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
if (this.#recaptchaTokens) {
|
|
987
|
-
this.onConfirm?.(this.#recaptchaTokens);
|
|
988
|
-
this.close();
|
|
989
|
-
}
|
|
990
981
|
});
|
|
991
982
|
this.querySelector("#apple-pay-modal-close")?.addEventListener(
|
|
992
983
|
"click",
|
|
993
984
|
() => this.close()
|
|
994
985
|
);
|
|
995
986
|
}
|
|
996
|
-
open(
|
|
997
|
-
this.#
|
|
987
|
+
open() {
|
|
988
|
+
this.#isClosed = false;
|
|
998
989
|
this.#triggerElement = document.activeElement;
|
|
999
990
|
document.body.appendChild(this);
|
|
1000
991
|
this.#keyHandler = (event) => {
|
|
@@ -1009,6 +1000,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
1009
1000
|
this.querySelector("#apple-pay-modal")?.focus();
|
|
1010
1001
|
}
|
|
1011
1002
|
close() {
|
|
1003
|
+
this.#isClosed = true;
|
|
1012
1004
|
if (this.#keyHandler) {
|
|
1013
1005
|
document.removeEventListener("keydown", this.#keyHandler, {
|
|
1014
1006
|
capture: true
|
|
@@ -1421,7 +1413,10 @@ function startApplePaySession(config) {
|
|
|
1421
1413
|
};
|
|
1422
1414
|
session.onpaymentauthorized = (event) => {
|
|
1423
1415
|
trackWalletPaymentAuthorised(config.skuId);
|
|
1424
|
-
config.onPaymentAuthorized(event, session)
|
|
1416
|
+
config.onPaymentAuthorized(event, session).catch(() => {
|
|
1417
|
+
session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
|
|
1418
|
+
redirectToFallbackCheckout(config);
|
|
1419
|
+
});
|
|
1425
1420
|
};
|
|
1426
1421
|
session.oncancel = () => {
|
|
1427
1422
|
config.onCancel?.();
|
|
@@ -1972,7 +1967,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
1972
1967
|
#returnUrl = "";
|
|
1973
1968
|
#supportedBillingCountries = [];
|
|
1974
1969
|
#entryPoint = "";
|
|
1975
|
-
#recaptchaTokensPromise = null;
|
|
1976
1970
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
1977
1971
|
get offer() {
|
|
1978
1972
|
return this.#offer;
|
|
@@ -2037,7 +2031,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2037
2031
|
this.#paymentOptions = null;
|
|
2038
2032
|
this.#returnUrl = "";
|
|
2039
2033
|
this.#supportedBillingCountries = [];
|
|
2040
|
-
this.#recaptchaTokensPromise = null;
|
|
2041
2034
|
this.querySelector(SEL_CONTAINER)?.remove();
|
|
2042
2035
|
}
|
|
2043
2036
|
// ─── Render pipeline ────────────────────────────────────────────────────────
|
|
@@ -2140,7 +2133,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2140
2133
|
if (button) {
|
|
2141
2134
|
button.classList.remove("wap-loading");
|
|
2142
2135
|
this.#updateDisabledState();
|
|
2143
|
-
this.#scheduleRecaptchaTokens();
|
|
2144
2136
|
trackExpressWalletImpression(this.#offer.sku_id, {
|
|
2145
2137
|
entryPoint: this.#entryPoint,
|
|
2146
2138
|
country: this.#country
|
|
@@ -2177,27 +2169,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2177
2169
|
}
|
|
2178
2170
|
const skuId = this.#offer.sku_id;
|
|
2179
2171
|
const country = this.#country;
|
|
2180
|
-
modal.onConfirm = (
|
|
2181
|
-
this.#initiateApplePay(skuId, country,
|
|
2182
|
-
this.#scheduleRecaptchaTokens();
|
|
2172
|
+
modal.onConfirm = (recaptchaTokensPromise) => {
|
|
2173
|
+
this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
|
|
2183
2174
|
};
|
|
2184
|
-
modal.open(
|
|
2185
|
-
}
|
|
2186
|
-
/**
|
|
2187
|
-
* Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
|
|
2188
|
-
* If the fetch rejects, the field is cleared so the next `modal.open()` call
|
|
2189
|
-
* falls back to a new attempt via the modal's own null-fallback path — preventing
|
|
2190
|
-
* a single transient failure from permanently disabling the CTA on all retries.
|
|
2191
|
-
* The identity guard prevents a late rejection from clobbering a newer attempt.
|
|
2192
|
-
*/
|
|
2193
|
-
#scheduleRecaptchaTokens() {
|
|
2194
|
-
const attempt = getPurchaseRecaptchaTokens();
|
|
2195
|
-
attempt.catch(() => {
|
|
2196
|
-
if (this.#recaptchaTokensPromise === attempt) {
|
|
2197
|
-
this.#recaptchaTokensPromise = null;
|
|
2198
|
-
}
|
|
2199
|
-
});
|
|
2200
|
-
this.#recaptchaTokensPromise = attempt;
|
|
2175
|
+
modal.open();
|
|
2201
2176
|
}
|
|
2202
2177
|
async #fetchSession() {
|
|
2203
2178
|
try {
|
|
@@ -2218,7 +2193,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2218
2193
|
return null;
|
|
2219
2194
|
}
|
|
2220
2195
|
}
|
|
2221
|
-
#buildSessionParams(skuId, country, sessionData, paymentOptions,
|
|
2196
|
+
#buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
|
|
2222
2197
|
const offer = this.#offer;
|
|
2223
2198
|
const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
|
|
2224
2199
|
return {
|
|
@@ -2230,30 +2205,41 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2230
2205
|
skuId,
|
|
2231
2206
|
returnUrl: this.#returnUrl,
|
|
2232
2207
|
userLoggedIn: sessionData.loggedIn,
|
|
2233
|
-
onPaymentAuthorized: (event, session) =>
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2208
|
+
onPaymentAuthorized: async (event, session) => {
|
|
2209
|
+
const recaptchaTokens = await recaptchaTokensPromise;
|
|
2210
|
+
return handlePaymentAuthorized(event, session, sessionData, {
|
|
2211
|
+
skuId,
|
|
2212
|
+
country,
|
|
2213
|
+
currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
|
|
2214
|
+
returnUrl: this.#returnUrl,
|
|
2215
|
+
supportedBillingCountries: this.#supportedBillingCountries,
|
|
2216
|
+
recaptchaTokens
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2241
2219
|
};
|
|
2242
2220
|
}
|
|
2243
|
-
#initiateApplePay(skuId, country,
|
|
2221
|
+
#initiateApplePay(skuId, country, recaptchaTokensPromise) {
|
|
2244
2222
|
try {
|
|
2245
2223
|
const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
|
|
2246
2224
|
const paymentOptions = this.#paymentOptions;
|
|
2225
|
+
if (!paymentOptions) {
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2247
2228
|
startApplePaySession(
|
|
2248
2229
|
this.#buildSessionParams(
|
|
2249
2230
|
skuId,
|
|
2250
2231
|
country,
|
|
2251
2232
|
sessionData,
|
|
2252
2233
|
paymentOptions,
|
|
2253
|
-
|
|
2234
|
+
recaptchaTokensPromise
|
|
2254
2235
|
)
|
|
2255
2236
|
);
|
|
2256
2237
|
} catch {
|
|
2238
|
+
redirectToFallbackCheckout({
|
|
2239
|
+
returnUrl: this.#returnUrl,
|
|
2240
|
+
skuId,
|
|
2241
|
+
countryCode: country
|
|
2242
|
+
});
|
|
2257
2243
|
}
|
|
2258
2244
|
}
|
|
2259
2245
|
};
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import type { Offer } from './types/offer.types';
|
|
2
|
-
import {
|
|
3
|
-
/** Resolved reCAPTCHA token set, mirroring the recaptcha utility return type. */
|
|
4
|
-
type RecaptchaTokens = Awaited<ReturnType<typeof getPurchaseRecaptchaTokens>>;
|
|
2
|
+
import { type RecaptchaTokens } from './utils/recaptcha';
|
|
5
3
|
export declare class ApplePayModal extends HTMLElement {
|
|
6
4
|
#private;
|
|
7
5
|
static readonly tag = "teg-apple-pay-modal";
|
|
8
6
|
static define(): void;
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* until the token pre-fetch resolves, so this callback is never reached
|
|
17
|
-
* without valid tokens.
|
|
8
|
+
* Callback invoked synchronously from the CTA click handler.
|
|
9
|
+
* Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
|
|
10
|
+
* by the time this is called. The caller (ApplePayButton) must call
|
|
11
|
+
* new ApplePaySession() synchronously inside this callback to stay within
|
|
12
|
+
* Safari's user-gesture window. The token promise resolves before
|
|
13
|
+
* onPaymentAuthorized fires (user still needs to authenticate on the sheet).
|
|
18
14
|
*/
|
|
19
|
-
onConfirm: ((
|
|
15
|
+
onConfirm: ((recaptchaTokensPromise: Promise<RecaptchaTokens>) => void) | null;
|
|
20
16
|
set offer(value: Offer | null);
|
|
21
17
|
get country(): string;
|
|
22
18
|
set country(value: string);
|
|
23
19
|
connectedCallback(): void;
|
|
24
|
-
open(
|
|
20
|
+
open(): void;
|
|
25
21
|
close(): void;
|
|
26
22
|
}
|
|
27
|
-
export {};
|
|
@@ -570,10 +570,9 @@ var loadRecaptchaScript = (siteKey) => {
|
|
|
570
570
|
);
|
|
571
571
|
if (existingScript) {
|
|
572
572
|
existingScript.addEventListener("load", () => resolve());
|
|
573
|
-
existingScript.addEventListener(
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
);
|
|
573
|
+
existingScript.addEventListener("error", () => {
|
|
574
|
+
reject(new Error("Failed to load existing reCAPTCHA script"));
|
|
575
|
+
});
|
|
577
576
|
return;
|
|
578
577
|
}
|
|
579
578
|
const script = document.createElement("script");
|
|
@@ -581,7 +580,9 @@ var loadRecaptchaScript = (siteKey) => {
|
|
|
581
580
|
script.async = true;
|
|
582
581
|
script.defer = true;
|
|
583
582
|
script.onload = () => resolve();
|
|
584
|
-
script.onerror = () =>
|
|
583
|
+
script.onerror = () => {
|
|
584
|
+
reject(new Error("Failed to load reCAPTCHA script"));
|
|
585
|
+
};
|
|
585
586
|
document.head.appendChild(script);
|
|
586
587
|
});
|
|
587
588
|
};
|
|
@@ -830,17 +831,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
830
831
|
#country = "";
|
|
831
832
|
#triggerElement = null;
|
|
832
833
|
#keyHandler = null;
|
|
833
|
-
#
|
|
834
|
-
#recaptchaTokensPromise = null;
|
|
834
|
+
#isClosed = false;
|
|
835
835
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
* until the token pre-fetch resolves, so this callback is never reached
|
|
843
|
-
* without valid tokens.
|
|
836
|
+
* Callback invoked synchronously from the CTA click handler.
|
|
837
|
+
* Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
|
|
838
|
+
* by the time this is called. The caller (ApplePayButton) must call
|
|
839
|
+
* new ApplePaySession() synchronously inside this callback to stay within
|
|
840
|
+
* Safari's user-gesture window. The token promise resolves before
|
|
841
|
+
* onPaymentAuthorized fires (user still needs to authenticate on the sheet).
|
|
844
842
|
*/
|
|
845
843
|
onConfirm = null;
|
|
846
844
|
set offer(value) {
|
|
@@ -937,28 +935,32 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
937
935
|
}
|
|
938
936
|
});
|
|
939
937
|
}
|
|
940
|
-
|
|
941
|
-
if (cta)
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
938
|
+
cta?.addEventListener("click", () => {
|
|
939
|
+
if (cta.disabled) return;
|
|
940
|
+
if (isUS && checkbox && !checkbox.checked) {
|
|
941
|
+
errorElement?.classList.add("apm__error--visible");
|
|
942
|
+
checkboxLabel?.classList.add("apm__checkbox-label--error");
|
|
943
|
+
return;
|
|
945
944
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
945
|
+
errorElement?.classList.remove("apm__error--visible");
|
|
946
|
+
this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
|
|
947
|
+
cta.disabled = true;
|
|
948
|
+
cta.setAttribute("aria-busy", "true");
|
|
949
|
+
const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
|
|
950
|
+
if (!this.#isClosed && this.onConfirm) {
|
|
951
|
+
this.onConfirm(recaptchaTokensPromise);
|
|
952
|
+
this.close();
|
|
953
|
+
}
|
|
954
|
+
recaptchaTokensPromise.catch(() => {
|
|
955
|
+
if (this.#isClosed) return;
|
|
956
|
+
cta.disabled = false;
|
|
957
|
+
cta.removeAttribute("aria-busy");
|
|
957
958
|
const existingError = this.querySelector("#apple-pay-modal-error");
|
|
958
959
|
if (existingError) {
|
|
959
960
|
const icon = existingError.querySelector("svg");
|
|
960
961
|
existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
|
|
961
962
|
if (icon) existingError.prepend(icon);
|
|
963
|
+
existingError.setAttribute("role", "alert");
|
|
962
964
|
existingError.classList.add("apm__error--visible");
|
|
963
965
|
} else {
|
|
964
966
|
const errorNotice = document.createElement("div");
|
|
@@ -968,25 +970,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
968
970
|
cta?.insertAdjacentElement("beforebegin", errorNotice);
|
|
969
971
|
}
|
|
970
972
|
});
|
|
971
|
-
}
|
|
972
|
-
cta?.addEventListener("click", () => {
|
|
973
|
-
if (isUS && checkbox && !checkbox.checked) {
|
|
974
|
-
errorElement?.classList.add("apm__error--visible");
|
|
975
|
-
checkboxLabel?.classList.add("apm__checkbox-label--error");
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
if (this.#recaptchaTokens) {
|
|
979
|
-
this.onConfirm?.(this.#recaptchaTokens);
|
|
980
|
-
this.close();
|
|
981
|
-
}
|
|
982
973
|
});
|
|
983
974
|
this.querySelector("#apple-pay-modal-close")?.addEventListener(
|
|
984
975
|
"click",
|
|
985
976
|
() => this.close()
|
|
986
977
|
);
|
|
987
978
|
}
|
|
988
|
-
open(
|
|
989
|
-
this.#
|
|
979
|
+
open() {
|
|
980
|
+
this.#isClosed = false;
|
|
990
981
|
this.#triggerElement = document.activeElement;
|
|
991
982
|
document.body.appendChild(this);
|
|
992
983
|
this.#keyHandler = (event) => {
|
|
@@ -1001,6 +992,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
1001
992
|
this.querySelector("#apple-pay-modal")?.focus();
|
|
1002
993
|
}
|
|
1003
994
|
close() {
|
|
995
|
+
this.#isClosed = true;
|
|
1004
996
|
if (this.#keyHandler) {
|
|
1005
997
|
document.removeEventListener("keydown", this.#keyHandler, {
|
|
1006
998
|
capture: true
|