@4site/engrid-scripts 0.25.9 → 0.25.12
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/a11y.d.ts +26 -4
- package/dist/a11y.js +70 -15
- package/dist/ecard.d.ts +4 -0
- package/dist/ecard.js +196 -1
- package/dist/interfaces/options.d.ts +3 -0
- package/dist/remember-me.d.ts +65 -0
- package/dist/remember-me.js +323 -17
- package/dist/supporter-hub.d.ts +41 -1
- package/dist/supporter-hub.js +227 -39
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/supporter-hub.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Component that adds 4Site Special Features to the Supporter Hub Page
|
|
2
|
-
import { ENGrid, EngridLogger, EnForm } from ".";
|
|
2
|
+
import { ENGrid, EngridLogger, EnForm, A11y } from ".";
|
|
3
3
|
export class SupporterHub {
|
|
4
4
|
constructor() {
|
|
5
5
|
this.logger = new EngridLogger("SupporterHub", "black", "pink", "🛖");
|
|
@@ -10,6 +10,9 @@ export class SupporterHub {
|
|
|
10
10
|
this.watch();
|
|
11
11
|
this.preventDuplicateSubmits();
|
|
12
12
|
this.pageAltsAndArias();
|
|
13
|
+
if (ENGrid.getPageNumber() === 1) {
|
|
14
|
+
this.announceLoginResponses();
|
|
15
|
+
}
|
|
13
16
|
}
|
|
14
17
|
shoudRun() {
|
|
15
18
|
return ("pageJson" in window &&
|
|
@@ -28,9 +31,10 @@ export class SupporterHub {
|
|
|
28
31
|
if (overlay.classList.contains("en__hubOverlay") ||
|
|
29
32
|
overlay.classList.contains("en__hubPledge__panels")) {
|
|
30
33
|
this.logger.log("Overlay found");
|
|
31
|
-
this.creditCardUpdate(
|
|
32
|
-
this.amountLabelUpdate(
|
|
33
|
-
this.dialogAltsAndArias(
|
|
34
|
+
this.creditCardUpdate(overlay);
|
|
35
|
+
this.amountLabelUpdate(overlay);
|
|
36
|
+
this.dialogAltsAndArias(overlay);
|
|
37
|
+
this.accessibilityScan(overlay);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
});
|
|
@@ -40,7 +44,11 @@ export class SupporterHub {
|
|
|
40
44
|
if (overlay.classList.contains("en__hubOverlay") ||
|
|
41
45
|
overlay.classList.contains("en__hubPledge__panels")) {
|
|
42
46
|
this.logger.log("Overlay removed");
|
|
43
|
-
|
|
47
|
+
A11y.inertPage(false);
|
|
48
|
+
const remainingOverlay = document.querySelector(".en__hubOverlay, .en__hubPledge__panels");
|
|
49
|
+
if (remainingOverlay) {
|
|
50
|
+
A11y.inertPage(true, remainingOverlay);
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
});
|
|
@@ -58,6 +66,7 @@ export class SupporterHub {
|
|
|
58
66
|
this.creditCardUpdate(hubOverlay);
|
|
59
67
|
this.amountLabelUpdate(hubOverlay);
|
|
60
68
|
this.dialogAltsAndArias(hubOverlay);
|
|
69
|
+
this.accessibilityScan(hubOverlay);
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
pageAltsAndArias() {
|
|
@@ -86,6 +95,208 @@ export class SupporterHub {
|
|
|
86
95
|
});
|
|
87
96
|
}
|
|
88
97
|
});
|
|
98
|
+
const emailField = document.querySelector('.en__field--hublogin');
|
|
99
|
+
if (emailField) {
|
|
100
|
+
const label = emailField.querySelector('label');
|
|
101
|
+
const input = emailField.querySelector('input');
|
|
102
|
+
if (label && input) {
|
|
103
|
+
const slug = ENGrid.slugify(label.innerText);
|
|
104
|
+
const labelId = `hublogin-label-${slug}`;
|
|
105
|
+
label.setAttribute("id", labelId);
|
|
106
|
+
input.setAttribute("aria-labelledby", labelId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The Supporter Hub login form shows success / failure / loading messages by
|
|
112
|
+
* toggling the inline `display` on static `.en__hubgadget__response` divs.
|
|
113
|
+
* Wire the login body up to the shared response announcer.
|
|
114
|
+
*/
|
|
115
|
+
announceLoginResponses() {
|
|
116
|
+
const body = document.querySelector(".en__supporterHubLogin__body");
|
|
117
|
+
if (!body)
|
|
118
|
+
return;
|
|
119
|
+
const emailInput = body.querySelector(".en__field--hublogin input");
|
|
120
|
+
const emailField = emailInput === null || emailInput === void 0 ? void 0 : emailInput.closest(".en__field--hublogin");
|
|
121
|
+
// Login failures are about the email field: flag it invalid and point AT at
|
|
122
|
+
// the alert describing why. This is login-specific, so it lives here rather
|
|
123
|
+
// than in the shared announcer.
|
|
124
|
+
this.announceHubResponses(body, {
|
|
125
|
+
onResult: (isFailure, alertRegionId) => {
|
|
126
|
+
if (emailInput) {
|
|
127
|
+
if (isFailure) {
|
|
128
|
+
emailInput.setAttribute("aria-invalid", "true");
|
|
129
|
+
emailInput.setAttribute("aria-describedby", alertRegionId);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
emailInput.removeAttribute("aria-invalid");
|
|
133
|
+
emailInput.removeAttribute("aria-describedby");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
emailField === null || emailField === void 0 ? void 0 : emailField.classList.toggle("en__field--validationFailed", isFailure);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Mirror `.en__hubgadget__response` messages within `container` into screen
|
|
142
|
+
* reader live regions. Engaging Networks owns these responses, and assistive
|
|
143
|
+
* tech doesn't reliably announce content that was already in the DOM and
|
|
144
|
+
* merely flipped to visible — and in hub overlays the responses don't even
|
|
145
|
+
* exist until a submit injects them.
|
|
146
|
+
*
|
|
147
|
+
* We append a polite (`role="status"`) and an assertive (`role="alert"`) live
|
|
148
|
+
* region, hide the originals from AT, then on any childList / display / class
|
|
149
|
+
* change we debounce and announce whichever response is currently visible,
|
|
150
|
+
* preferring a terminal result over the transient "Loading" so an instant
|
|
151
|
+
* failure isn't preceded by a stray "Loading". Failures (and, for overlays,
|
|
152
|
+
* all messages) use the assertive region; everything else uses the polite one.
|
|
153
|
+
*
|
|
154
|
+
* This is a form-agnostic engine: callers supply their own side effects via
|
|
155
|
+
* `onResult` rather than this method knowing about any specific form.
|
|
156
|
+
*
|
|
157
|
+
* @param container The element that holds (or will hold) the responses.
|
|
158
|
+
* @param options `preferAssertive` announces success assertively too —
|
|
159
|
+
* overlays redraw their content (and shift focus) on success,
|
|
160
|
+
* which makes screen readers drop polite announcements, so
|
|
161
|
+
* assertive ones are used to survive that. `onResult` is fired
|
|
162
|
+
* with each settled result (and the assertive region's id) so
|
|
163
|
+
* callers can react — e.g. the login form flags its email
|
|
164
|
+
* field invalid — without baking that into the engine.
|
|
165
|
+
*/
|
|
166
|
+
announceHubResponses(container, options = {}) {
|
|
167
|
+
var _a;
|
|
168
|
+
const { preferAssertive = false, onResult } = options;
|
|
169
|
+
// Guard against wiring the same container up twice.
|
|
170
|
+
if (container.dataset.engridResponseAnnounce === "true")
|
|
171
|
+
return;
|
|
172
|
+
container.dataset.engridResponseAnnounce = "true";
|
|
173
|
+
// The overlay popup is an `aria-modal="true"` dialog, which tells assistive
|
|
174
|
+
// tech to ignore everything outside it. Live regions appended to the outer
|
|
175
|
+
// .en__hubOverlay (a sibling of the dialog) are therefore never announced.
|
|
176
|
+
// Anchor the regions inside the dialog so they live within the modal scope.
|
|
177
|
+
// Falls back to the container itself for the non-modal login form.
|
|
178
|
+
const regionAnchor = (_a = container.querySelector(".en__hubOverlay__popup")) !== null && _a !== void 0 ? _a : container;
|
|
179
|
+
const makeRegion = (assertive) => {
|
|
180
|
+
const region = document.createElement("div");
|
|
181
|
+
region.setAttribute("role", assertive ? "alert" : "status");
|
|
182
|
+
region.setAttribute("aria-atomic", "true");
|
|
183
|
+
region.classList.add("engrid__sr-only");
|
|
184
|
+
return regionAnchor.appendChild(region);
|
|
185
|
+
};
|
|
186
|
+
const politeRegion = makeRegion(false);
|
|
187
|
+
const assertiveRegion = makeRegion(true);
|
|
188
|
+
assertiveRegion.id = `en__hubgadget__response--failure-alert-${Math.random()
|
|
189
|
+
.toString(36)
|
|
190
|
+
.slice(2, 7)}`;
|
|
191
|
+
const isVisible = (el) => window.getComputedStyle(el).display !== "none";
|
|
192
|
+
// The live regions own announcements; hide the originals so each message is
|
|
193
|
+
// read once rather than twice. Responses can be injected after the fact
|
|
194
|
+
// (overlays), so re-hide on every pass, but only write when needed to avoid
|
|
195
|
+
// retriggering our own observer.
|
|
196
|
+
const hideResponses = () => {
|
|
197
|
+
container
|
|
198
|
+
.querySelectorAll(".en__hubgadget__response")
|
|
199
|
+
.forEach((r) => {
|
|
200
|
+
if (r.getAttribute("aria-hidden") !== "true") {
|
|
201
|
+
r.setAttribute("aria-hidden", "true");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
// Clear then set on the next frame so the screen reader registers a fresh
|
|
206
|
+
// change even when the same message is re-stated after a content redraw.
|
|
207
|
+
const speak = (region, text) => {
|
|
208
|
+
region.textContent = "";
|
|
209
|
+
window.requestAnimationFrame(() => {
|
|
210
|
+
region.textContent = text;
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
let lastAnnounced = "";
|
|
214
|
+
// Set when a response node is added/removed (i.e. the overlay redrew its
|
|
215
|
+
// content). A polite announcement made mid-redraw gets dropped, so we must
|
|
216
|
+
// re-state the message once things settle even if the text is unchanged.
|
|
217
|
+
let forceReannounce = false;
|
|
218
|
+
const announce = () => {
|
|
219
|
+
var _a;
|
|
220
|
+
hideResponses();
|
|
221
|
+
const responses = Array.from(container.querySelectorAll(".en__hubgadget__response"));
|
|
222
|
+
const visible = responses.filter(isVisible);
|
|
223
|
+
const target = (_a = visible.find((r) => !r.classList.contains("en__hubgadget__response--loading"))) !== null && _a !== void 0 ? _a : visible[0];
|
|
224
|
+
const message = target ? (target.textContent || "").trim() : "";
|
|
225
|
+
const isFailure = !!(target === null || target === void 0 ? void 0 : target.classList.contains("en__hubgadget__response--failure"));
|
|
226
|
+
const reannounce = forceReannounce;
|
|
227
|
+
forceReannounce = false;
|
|
228
|
+
if (message === lastAnnounced && !reannounce)
|
|
229
|
+
return;
|
|
230
|
+
lastAnnounced = message;
|
|
231
|
+
if (!message) {
|
|
232
|
+
politeRegion.textContent = "";
|
|
233
|
+
assertiveRegion.textContent = "";
|
|
234
|
+
onResult === null || onResult === void 0 ? void 0 : onResult(false, assertiveRegion.id);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.logger.log(`Announcing hub response: ${message}`);
|
|
238
|
+
// Overlays must announce assertively (polite is dropped on their content
|
|
239
|
+
// redraw); the login form keeps polite for non-failures.
|
|
240
|
+
const useAssertive = isFailure || preferAssertive;
|
|
241
|
+
if (useAssertive) {
|
|
242
|
+
politeRegion.textContent = "";
|
|
243
|
+
speak(assertiveRegion, message);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
assertiveRegion.textContent = "";
|
|
247
|
+
speak(politeRegion, message);
|
|
248
|
+
}
|
|
249
|
+
onResult === null || onResult === void 0 ? void 0 : onResult(isFailure, assertiveRegion.id);
|
|
250
|
+
};
|
|
251
|
+
// Only react to mutations that actually involve a response. The container
|
|
252
|
+
// (especially an overlay) is full of unrelated churn — form fields, the
|
|
253
|
+
// loading spinner, the a11y error regions — and our own live-region writes
|
|
254
|
+
// are children of it too. Reacting to all of that would perpetually reset
|
|
255
|
+
// the debounce and drown out the real announcement.
|
|
256
|
+
const isResponse = (node) => node instanceof HTMLElement &&
|
|
257
|
+
(node.classList.contains("en__hubgadget__response") ||
|
|
258
|
+
!!node.querySelector(".en__hubgadget__response"));
|
|
259
|
+
const isRelevant = (record) => {
|
|
260
|
+
if (record.type === "childList") {
|
|
261
|
+
return (Array.from(record.addedNodes).some(isResponse) ||
|
|
262
|
+
Array.from(record.removedNodes).some(isResponse));
|
|
263
|
+
}
|
|
264
|
+
// attribute (style/class) change directly on a response element
|
|
265
|
+
return (record.target instanceof HTMLElement &&
|
|
266
|
+
record.target.classList.contains("en__hubgadget__response"));
|
|
267
|
+
};
|
|
268
|
+
let debounce = 0;
|
|
269
|
+
new MutationObserver((records) => {
|
|
270
|
+
const relevant = records.filter(isRelevant);
|
|
271
|
+
if (!relevant.length)
|
|
272
|
+
return;
|
|
273
|
+
// A response node being added/removed means the content was redrawn;
|
|
274
|
+
// force a re-announcement so a message dropped mid-redraw is re-stated.
|
|
275
|
+
if (relevant.some((r) => r.type === "childList")) {
|
|
276
|
+
forceReannounce = true;
|
|
277
|
+
}
|
|
278
|
+
window.clearTimeout(debounce);
|
|
279
|
+
debounce = window.setTimeout(announce, 250);
|
|
280
|
+
}).observe(container, {
|
|
281
|
+
attributes: true,
|
|
282
|
+
attributeFilter: ["style", "class"],
|
|
283
|
+
childList: true,
|
|
284
|
+
subtree: true,
|
|
285
|
+
});
|
|
286
|
+
announce(); // catch any response already visible at setup
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Run the field-level accessibility scan and response announcer against a
|
|
290
|
+
* freshly opened overlay. EN injects overlay markup (and its fields) when the
|
|
291
|
+
* gadget is clicked, so A11y's constructor-time sweep never sees them. The
|
|
292
|
+
* scan is idempotent and the announcer guards against double-wiring, so a
|
|
293
|
+
* delayed pass — matching the timing of the other overlay handlers — is safe.
|
|
294
|
+
*/
|
|
295
|
+
accessibilityScan(overlay) {
|
|
296
|
+
window.setTimeout(() => {
|
|
297
|
+
A11y.scanFields(overlay);
|
|
298
|
+
this.announceHubResponses(overlay, { preferAssertive: true });
|
|
299
|
+
}, 300);
|
|
89
300
|
}
|
|
90
301
|
creditCardUpdate(overlay) {
|
|
91
302
|
window.setTimeout(() => {
|
|
@@ -115,12 +326,22 @@ export class SupporterHub {
|
|
|
115
326
|
}
|
|
116
327
|
dialogAltsAndArias(overlay) {
|
|
117
328
|
window.setTimeout(() => {
|
|
118
|
-
|
|
329
|
+
const hubOverlay = overlay.classList.contains("en__hubOverlay")
|
|
330
|
+
? overlay
|
|
331
|
+
: document.querySelector(".en__hubOverlay") ||
|
|
332
|
+
overlay;
|
|
333
|
+
A11y.inertPage(true, hubOverlay);
|
|
119
334
|
const header = overlay.querySelector(".en__hubOverlay__header"), closeButton = header.querySelector("a");
|
|
120
335
|
// Tag close button
|
|
121
336
|
if (header && closeButton) {
|
|
122
337
|
closeButton.setAttribute("role", "button");
|
|
123
338
|
closeButton.setAttribute("aria-label", "Close");
|
|
339
|
+
document.addEventListener("keydown", (e) => {
|
|
340
|
+
if (e.key === "Escape") {
|
|
341
|
+
this.logger.log("Escape key pressed, closing overlay");
|
|
342
|
+
closeButton.click();
|
|
343
|
+
}
|
|
344
|
+
}, { once: true });
|
|
124
345
|
}
|
|
125
346
|
// Tag header and label dialog
|
|
126
347
|
const headerTitle = header.querySelector("h2");
|
|
@@ -144,39 +365,6 @@ export class SupporterHub {
|
|
|
144
365
|
}
|
|
145
366
|
}, 300);
|
|
146
367
|
}
|
|
147
|
-
inertPage(inert, overlay) {
|
|
148
|
-
if (inert) {
|
|
149
|
-
const hubOverlay = overlay && overlay.classList.contains("en__hubOverlay")
|
|
150
|
-
? overlay
|
|
151
|
-
: document.querySelector(".en__hubOverlay") ||
|
|
152
|
-
overlay;
|
|
153
|
-
if (!hubOverlay)
|
|
154
|
-
return;
|
|
155
|
-
let element = hubOverlay;
|
|
156
|
-
while (element && element !== document.body) {
|
|
157
|
-
const parent = element.parentElement;
|
|
158
|
-
if (parent) {
|
|
159
|
-
Array.from(parent.children).forEach((sibling) => {
|
|
160
|
-
if (sibling !== element &&
|
|
161
|
-
sibling instanceof HTMLElement &&
|
|
162
|
-
!sibling.hasAttribute("inert")) {
|
|
163
|
-
sibling.setAttribute("inert", "");
|
|
164
|
-
sibling.dataset.engridInert = "true";
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
element = parent;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
else if (!document.querySelector(".en__hubOverlay, .en__hubPledge__panels")) {
|
|
172
|
-
document
|
|
173
|
-
.querySelectorAll("[data-engrid-inert]")
|
|
174
|
-
.forEach((element) => {
|
|
175
|
-
element.removeAttribute("inert");
|
|
176
|
-
delete element.dataset.engridInert;
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
368
|
// The supporter hub does not properly handle or prevent duplicate submits, so we add a listener to prevent this.
|
|
181
369
|
preventDuplicateSubmits() {
|
|
182
370
|
document.addEventListener("click", (e) => {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AppVersion = "0.25.
|
|
1
|
+
export declare const AppVersion = "0.25.12";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AppVersion = "0.25.
|
|
1
|
+
export const AppVersion = "0.25.12";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4site/engrid-scripts",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.12",
|
|
4
4
|
"description": "Scripts for the ENGRID Project",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"strongly-typed-events": "^2.0.9"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/4site-interactive-studios/engrid#readme",
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "79a43bee4e541f226dd438124f7a0d14f3169586",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"nodemon": "^2.0.20"
|
|
31
31
|
}
|