@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.
@@ -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(node);
32
- this.amountLabelUpdate(node);
33
- this.dialogAltsAndArias(node);
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
- this.inertPage(false);
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
- this.inertPage(true, overlay);
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.9";
1
+ export declare const AppVersion = "0.25.12";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const AppVersion = "0.25.9";
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.9",
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": "18896dd2e4e4ed893ac2531f2fc01586fd5af904",
28
+ "gitHead": "79a43bee4e541f226dd438124f7a0d14f3169586",
29
29
  "devDependencies": {
30
30
  "nodemon": "^2.0.20"
31
31
  }