@4site/engrid-scripts 0.27.2 → 0.27.4
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 +18 -10
- package/dist/a11y.js +206 -80
- package/dist/frequency-upsell-modal.d.ts +1 -1
- package/dist/frequency-upsell-modal.js +3 -2
- package/dist/frequency-upsell.d.ts +5 -0
- package/dist/frequency-upsell.js +15 -1
- package/dist/interfaces/frequency-upsell-options.d.ts +2 -0
- package/dist/interfaces/frequency-upsell-options.js +2 -0
- package/dist/modal.d.ts +9 -0
- package/dist/modal.js +25 -6
- package/dist/page-background.d.ts +5 -0
- package/dist/page-background.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/a11y.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare class A11y {
|
|
2
2
|
private logger;
|
|
3
3
|
private observer;
|
|
4
|
+
private liveRegionUpdateTimeout;
|
|
5
|
+
private shouldFocusFirstInvalidField;
|
|
4
6
|
constructor();
|
|
5
7
|
/**
|
|
6
|
-
* Apply the field-level accessibility tagging (error
|
|
8
|
+
* Apply the field-level accessibility tagging (error containers,
|
|
7
9
|
* aria-required, fallback aria-labels, radio group roles) to every field
|
|
8
10
|
* within `root`. Defaults to the whole document on initial load, but can be
|
|
9
11
|
* pointed at a freshly injected fragment (e.g. a Supporter Hub overlay) so
|
|
@@ -11,22 +13,23 @@ export declare class A11y {
|
|
|
11
13
|
* idempotent, so re-scanning already tagged fields is safe.
|
|
12
14
|
*/
|
|
13
15
|
static scanFields(root?: ParentNode): void;
|
|
14
|
-
private static
|
|
16
|
+
private static addGlobalErrorLiveRegion;
|
|
15
17
|
private static addGroupRole;
|
|
16
18
|
private static addRequired;
|
|
17
19
|
private static addLabel;
|
|
18
20
|
private updateFrequencyLabel;
|
|
19
21
|
private setAutoGeneratedAltTags;
|
|
20
22
|
/**
|
|
21
|
-
* Observe #engrid for .en__field__error additions and removals,
|
|
22
|
-
*
|
|
23
|
-
* aria-invalid / aria-describedby on the corresponding input
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Observe #engrid for .en__field__error additions and removals, relocating
|
|
24
|
+
* each error after its field's input so DOM order matches visual order. Also
|
|
25
|
+
* toggles aria-invalid / aria-describedby on the corresponding input (or radio
|
|
26
|
+
* group when the field has role="group") and updates a single, DOM-ordered
|
|
27
|
+
* global live region. Runs for the lifetime of the page so async validators
|
|
28
|
+
* (NeverBounce, VGS, server re-renders) are caught without timing assumptions.
|
|
26
29
|
*/
|
|
27
30
|
private observeErrorMessages;
|
|
28
|
-
private
|
|
29
|
-
private
|
|
31
|
+
private tagFieldError;
|
|
32
|
+
private clearFieldError;
|
|
30
33
|
/**
|
|
31
34
|
* Make everything on the page inert except the supplied overlay element and
|
|
32
35
|
* its ancestors. This hides background content from assistive technology and
|
|
@@ -40,5 +43,10 @@ export declare class A11y {
|
|
|
40
43
|
* `inert` is true; ignored when `inert` is false.
|
|
41
44
|
*/
|
|
42
45
|
static inertPage(inert: boolean, overlay?: HTMLElement): void;
|
|
43
|
-
private
|
|
46
|
+
private scheduleLiveRegionUpdate;
|
|
47
|
+
private updateGlobalErrorLiveRegion;
|
|
48
|
+
private formatErrorMessage;
|
|
49
|
+
private focusFirstInvalidField;
|
|
50
|
+
private getFieldLabel;
|
|
51
|
+
private normalizeLabel;
|
|
44
52
|
}
|
package/dist/a11y.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { EngridLogger } from "./logger";
|
|
2
|
+
import { ENGrid } from "./engrid";
|
|
3
|
+
import { EnForm } from "./events/en-form";
|
|
2
4
|
// a11y means accessibility
|
|
3
5
|
// This Component is supposed to be used as a helper for Aria Attributes & Other Accessibility Features
|
|
4
6
|
export class A11y {
|
|
5
7
|
constructor() {
|
|
8
|
+
var _a;
|
|
6
9
|
this.logger = new EngridLogger("A11y", "#FFFFFF", "#811212", "👁️🗨️");
|
|
7
10
|
this.observer = null;
|
|
11
|
+
this.liveRegionUpdateTimeout = null;
|
|
12
|
+
this.shouldFocusFirstInvalidField = false;
|
|
8
13
|
A11y.scanFields();
|
|
9
14
|
this.updateFrequencyLabel();
|
|
10
15
|
const ecardImages = document.querySelectorAll('.en__ecarditems__list img');
|
|
11
16
|
this.setAutoGeneratedAltTags(ecardImages);
|
|
12
|
-
this.manageErrorListAlertRole();
|
|
13
17
|
this.observeErrorMessages();
|
|
18
|
+
(_a = ENGrid.enForm) === null || _a === void 0 ? void 0 : _a.addEventListener('submit', () => {
|
|
19
|
+
this.shouldFocusFirstInvalidField = true;
|
|
20
|
+
});
|
|
21
|
+
// onSubmit only fires once validation has passed, so disarm the focus
|
|
22
|
+
// flag: a successful submit must not leave it set, or the next unrelated
|
|
23
|
+
// async field error (e.g. NeverBounce on blur) would steal focus.
|
|
24
|
+
EnForm.getInstance().onSubmit.subscribe(() => {
|
|
25
|
+
this.shouldFocusFirstInvalidField = false;
|
|
26
|
+
});
|
|
14
27
|
}
|
|
15
28
|
/**
|
|
16
|
-
* Apply the field-level accessibility tagging (error
|
|
29
|
+
* Apply the field-level accessibility tagging (error containers,
|
|
17
30
|
* aria-required, fallback aria-labels, radio group roles) to every field
|
|
18
31
|
* within `root`. Defaults to the whole document on initial load, but can be
|
|
19
32
|
* pointed at a freshly injected fragment (e.g. a Supporter Hub overlay) so
|
|
@@ -21,24 +34,29 @@ export class A11y {
|
|
|
21
34
|
* idempotent, so re-scanning already tagged fields is safe.
|
|
22
35
|
*/
|
|
23
36
|
static scanFields(root = document) {
|
|
24
|
-
A11y.
|
|
37
|
+
A11y.addGlobalErrorLiveRegion(root);
|
|
25
38
|
A11y.addRequired(root);
|
|
26
39
|
A11y.addLabel(root);
|
|
27
40
|
A11y.addGroupRole(root);
|
|
28
41
|
}
|
|
29
|
-
static
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
static addGlobalErrorLiveRegion(root = document) {
|
|
43
|
+
var _a;
|
|
44
|
+
const liveRegionId = 'engrid-a11y-error-summary';
|
|
45
|
+
if (document.getElementById(liveRegionId))
|
|
46
|
+
return;
|
|
47
|
+
const form = (_a = ENGrid.enForm) !== null && _a !== void 0 ? _a : root.querySelector('form.en__component');
|
|
48
|
+
if (!form)
|
|
49
|
+
return;
|
|
50
|
+
const region = document.createElement('div');
|
|
51
|
+
region.id = liveRegionId;
|
|
52
|
+
region.className = 'engrid__sr-only';
|
|
53
|
+
region.setAttribute('aria-live', 'polite');
|
|
54
|
+
region.setAttribute('aria-atomic', 'true');
|
|
55
|
+
form.insertBefore(region, form.firstChild);
|
|
56
|
+
const errorList = document.querySelector('ul.en__errorList');
|
|
57
|
+
if (errorList === null || errorList === void 0 ? void 0 : errorList.hasAttribute('role')) {
|
|
58
|
+
errorList.removeAttribute('role');
|
|
59
|
+
}
|
|
42
60
|
}
|
|
43
61
|
static addGroupRole(root = document) {
|
|
44
62
|
// Add role="group" to all EN Radio fields
|
|
@@ -52,6 +70,13 @@ export class A11y {
|
|
|
52
70
|
const label = field.querySelector("label");
|
|
53
71
|
if (label) {
|
|
54
72
|
label.setAttribute("id", `en__field__label--${Math.random().toString(36).slice(2, 7)}`);
|
|
73
|
+
// EN renders the group's question label as a <label>, but a radio group
|
|
74
|
+
// has no single control for `for` to point at. When EN leaves it blank
|
|
75
|
+
// (`for=""`) it's an invalid IDREF, so strip only that empty case
|
|
76
|
+
const forAttr = label.getAttribute("for");
|
|
77
|
+
if (forAttr !== null && forAttr.trim() === "") {
|
|
78
|
+
label.removeAttribute("for");
|
|
79
|
+
}
|
|
55
80
|
field.setAttribute("aria-labelledby", label.id);
|
|
56
81
|
}
|
|
57
82
|
});
|
|
@@ -128,11 +153,12 @@ export class A11y {
|
|
|
128
153
|
});
|
|
129
154
|
}
|
|
130
155
|
/**
|
|
131
|
-
* Observe #engrid for .en__field__error additions and removals,
|
|
132
|
-
*
|
|
133
|
-
* aria-invalid / aria-describedby on the corresponding input
|
|
134
|
-
*
|
|
135
|
-
*
|
|
156
|
+
* Observe #engrid for .en__field__error additions and removals, relocating
|
|
157
|
+
* each error after its field's input so DOM order matches visual order. Also
|
|
158
|
+
* toggles aria-invalid / aria-describedby on the corresponding input (or radio
|
|
159
|
+
* group when the field has role="group") and updates a single, DOM-ordered
|
|
160
|
+
* global live region. Runs for the lifetime of the page so async validators
|
|
161
|
+
* (NeverBounce, VGS, server re-renders) are caught without timing assumptions.
|
|
136
162
|
*/
|
|
137
163
|
observeErrorMessages() {
|
|
138
164
|
var _a;
|
|
@@ -143,22 +169,22 @@ export class A11y {
|
|
|
143
169
|
continue;
|
|
144
170
|
record.addedNodes.forEach(node => {
|
|
145
171
|
if (node instanceof HTMLElement && node.classList.contains('en__field__error')) {
|
|
146
|
-
this.
|
|
172
|
+
this.tagFieldError(node);
|
|
147
173
|
}
|
|
148
174
|
});
|
|
149
175
|
record.removedNodes.forEach(node => {
|
|
150
|
-
var _a, _b;
|
|
151
176
|
if (!(node instanceof HTMLElement))
|
|
152
177
|
return;
|
|
153
178
|
if (!node.classList.contains('en__field__error'))
|
|
154
179
|
return;
|
|
180
|
+
if (node.isConnected)
|
|
181
|
+
return; // relocated, not removed
|
|
155
182
|
// node.parentElement is null after removal; record.target is the
|
|
156
183
|
// former parent. Walk up to the enclosing .en__field to be defensive
|
|
157
184
|
// against deeper nesting.
|
|
158
|
-
const fieldWrapper =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.clearErrorMessage(alert);
|
|
185
|
+
const fieldWrapper = record.target.closest('.en__field');
|
|
186
|
+
if (fieldWrapper)
|
|
187
|
+
this.clearFieldError(fieldWrapper, node.id);
|
|
162
188
|
});
|
|
163
189
|
}
|
|
164
190
|
});
|
|
@@ -167,55 +193,76 @@ export class A11y {
|
|
|
167
193
|
// before this observer was attached.
|
|
168
194
|
document.querySelectorAll('.en__field').forEach(field => {
|
|
169
195
|
const error = field.querySelector('.en__field__error');
|
|
170
|
-
const alert = field.querySelector('.en__field__error__alert');
|
|
171
196
|
if (error)
|
|
172
|
-
this.
|
|
173
|
-
else if (alert)
|
|
174
|
-
this.clearErrorMessage(alert);
|
|
197
|
+
this.tagFieldError(error);
|
|
175
198
|
});
|
|
176
199
|
}
|
|
177
|
-
|
|
200
|
+
tagFieldError(error) {
|
|
178
201
|
var _a;
|
|
179
|
-
|
|
180
|
-
return;
|
|
181
|
-
const fieldWrapper = field.closest('.en__field');
|
|
202
|
+
const fieldWrapper = error.closest('.en__field');
|
|
182
203
|
if (!fieldWrapper)
|
|
183
204
|
return;
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
205
|
+
const fieldElement = fieldWrapper.querySelector('.en__field__element');
|
|
206
|
+
if (fieldElement && fieldElement.nextElementSibling !== error) {
|
|
207
|
+
fieldElement.insertAdjacentElement('afterend', error);
|
|
208
|
+
}
|
|
209
|
+
if (!error.id) {
|
|
210
|
+
error.id = `en__field__error--a11y-${Math.random().toString(36).slice(2, 7)}`;
|
|
211
|
+
}
|
|
212
|
+
this.scheduleLiveRegionUpdate();
|
|
213
|
+
const target = fieldWrapper.getAttribute('role') === 'group'
|
|
214
|
+
? fieldWrapper
|
|
215
|
+
: fieldWrapper.querySelector('.en__field__element input, .en__field__element select, .en__field__element textarea');
|
|
216
|
+
if (!target)
|
|
191
217
|
return;
|
|
192
|
-
|
|
193
|
-
const describedBy = ((_a =
|
|
218
|
+
target.setAttribute('aria-invalid', 'true');
|
|
219
|
+
const describedBy = ((_a = target.getAttribute('aria-describedby')) !== null && _a !== void 0 ? _a : '')
|
|
194
220
|
.split(/\s+/)
|
|
195
221
|
.filter(Boolean);
|
|
196
|
-
if (describedBy.indexOf(
|
|
197
|
-
describedBy.push(
|
|
222
|
+
if (describedBy.indexOf(error.id) === -1) {
|
|
223
|
+
describedBy.push(error.id);
|
|
224
|
+
}
|
|
225
|
+
target.setAttribute('aria-describedby', describedBy.join(' '));
|
|
226
|
+
// For radio groups, the group itself is the accessible widget; keep the
|
|
227
|
+
// individual inputs from also being announced as invalid.
|
|
228
|
+
if (target === fieldWrapper) {
|
|
229
|
+
fieldWrapper
|
|
230
|
+
.querySelectorAll('.en__field__element input, .en__field__element select, .en__field__element textarea')
|
|
231
|
+
.forEach(input => {
|
|
232
|
+
var _a;
|
|
233
|
+
input.removeAttribute('aria-invalid');
|
|
234
|
+
const remaining = ((_a = input.getAttribute('aria-describedby')) !== null && _a !== void 0 ? _a : '')
|
|
235
|
+
.split(/\s+/)
|
|
236
|
+
.filter(id => id && id !== error.id);
|
|
237
|
+
if (remaining.length) {
|
|
238
|
+
input.setAttribute('aria-describedby', remaining.join(' '));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
input.removeAttribute('aria-describedby');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
198
244
|
}
|
|
199
|
-
inputElement.setAttribute('aria-describedby', describedBy.join(' '));
|
|
200
245
|
}
|
|
201
|
-
|
|
246
|
+
clearFieldError(fieldWrapper, errorId) {
|
|
202
247
|
var _a;
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!inputElement)
|
|
248
|
+
this.scheduleLiveRegionUpdate();
|
|
249
|
+
const target = fieldWrapper.getAttribute('role') === 'group'
|
|
250
|
+
? fieldWrapper
|
|
251
|
+
: fieldWrapper.querySelector('.en__field__element input, .en__field__element select, .en__field__element textarea');
|
|
252
|
+
if (!target)
|
|
209
253
|
return;
|
|
210
|
-
|
|
211
|
-
|
|
254
|
+
const hasRemainingError = fieldWrapper.querySelector('.en__field__error') !== null;
|
|
255
|
+
if (!hasRemainingError) {
|
|
256
|
+
target.removeAttribute('aria-invalid');
|
|
257
|
+
}
|
|
258
|
+
const remaining = ((_a = target.getAttribute('aria-describedby')) !== null && _a !== void 0 ? _a : '')
|
|
212
259
|
.split(/\s+/)
|
|
213
|
-
.filter(id => id && id !==
|
|
260
|
+
.filter(id => id && id !== errorId);
|
|
214
261
|
if (remaining.length) {
|
|
215
|
-
|
|
262
|
+
target.setAttribute('aria-describedby', remaining.join(' '));
|
|
216
263
|
}
|
|
217
264
|
else {
|
|
218
|
-
|
|
265
|
+
target.removeAttribute('aria-describedby');
|
|
219
266
|
}
|
|
220
267
|
}
|
|
221
268
|
/**
|
|
@@ -259,29 +306,108 @@ export class A11y {
|
|
|
259
306
|
});
|
|
260
307
|
}
|
|
261
308
|
}
|
|
262
|
-
|
|
309
|
+
scheduleLiveRegionUpdate() {
|
|
310
|
+
if (this.liveRegionUpdateTimeout !== null) {
|
|
311
|
+
window.clearTimeout(this.liveRegionUpdateTimeout);
|
|
312
|
+
}
|
|
313
|
+
this.liveRegionUpdateTimeout = window.setTimeout(() => {
|
|
314
|
+
this.liveRegionUpdateTimeout = null;
|
|
315
|
+
this.updateGlobalErrorLiveRegion();
|
|
316
|
+
}, 100);
|
|
317
|
+
}
|
|
318
|
+
updateGlobalErrorLiveRegion() {
|
|
319
|
+
var _a;
|
|
320
|
+
const region = document.getElementById('engrid-a11y-error-summary');
|
|
321
|
+
if (!region)
|
|
322
|
+
return;
|
|
263
323
|
const errorList = document.querySelector('ul.en__errorList');
|
|
264
|
-
if (
|
|
324
|
+
if (errorList === null || errorList === void 0 ? void 0 : errorList.hasAttribute('role')) {
|
|
325
|
+
errorList.removeAttribute('role');
|
|
326
|
+
}
|
|
327
|
+
const fields = Array.from(document.querySelectorAll('.en__field'));
|
|
328
|
+
const messages = fields
|
|
329
|
+
.map(field => {
|
|
330
|
+
var _a;
|
|
331
|
+
const error = field.querySelector('.en__field__error');
|
|
332
|
+
if (!((_a = error === null || error === void 0 ? void 0 : error.textContent) === null || _a === void 0 ? void 0 : _a.trim()))
|
|
333
|
+
return null;
|
|
334
|
+
const label = this.getFieldLabel(field);
|
|
335
|
+
const message = this.formatErrorMessage(label, error.textContent.trim());
|
|
336
|
+
return message;
|
|
337
|
+
})
|
|
338
|
+
.filter((message) => Boolean(message));
|
|
339
|
+
// Top-of-form server errors (ul.en__errorList) aren't tied to a .en__field,
|
|
340
|
+
// so fold in any that aren't already covered by the per-field messages,
|
|
341
|
+
// otherwise they would be silent for screen readers.
|
|
342
|
+
const normalizeForCompare = (value) => value.replace(/\s+/g, ' ').replace(/[.!?:]+$/, '').trim().toLowerCase();
|
|
343
|
+
const coveredMessages = messages.map(normalizeForCompare).filter(Boolean);
|
|
344
|
+
const serverMessages = Array.from((_a = errorList === null || errorList === void 0 ? void 0 : errorList.querySelectorAll('li')) !== null && _a !== void 0 ? _a : [])
|
|
345
|
+
.map(item => { var _a, _b; return (_b = (_a = item.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ''; })
|
|
346
|
+
.filter(text => {
|
|
347
|
+
if (!text)
|
|
348
|
+
return false;
|
|
349
|
+
const normalized = normalizeForCompare(text);
|
|
350
|
+
if (!normalized)
|
|
351
|
+
return false;
|
|
352
|
+
return !coveredMessages.some(covered => covered === normalized ||
|
|
353
|
+
covered.includes(normalized) ||
|
|
354
|
+
normalized.includes(covered));
|
|
355
|
+
});
|
|
356
|
+
const allMessages = [...messages, ...serverMessages];
|
|
357
|
+
region.textContent = '';
|
|
358
|
+
if (!allMessages.length) {
|
|
359
|
+
this.shouldFocusFirstInvalidField = false;
|
|
265
360
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
361
|
+
}
|
|
362
|
+
if (allMessages.length === 1) {
|
|
363
|
+
region.textContent = allMessages[0];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const cleaned = allMessages.map(message => message.replace(/[.!?]+$/, '').trim());
|
|
367
|
+
region.textContent = `There are ${allMessages.length} errors: ${cleaned.join('. ')}.`;
|
|
368
|
+
}
|
|
369
|
+
if (this.shouldFocusFirstInvalidField) {
|
|
370
|
+
this.shouldFocusFirstInvalidField = false;
|
|
371
|
+
this.focusFirstInvalidField();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
formatErrorMessage(label, message) {
|
|
375
|
+
const cleanMessage = message.trim();
|
|
376
|
+
const cleanLabel = this.normalizeLabel(label);
|
|
377
|
+
if (!cleanLabel)
|
|
378
|
+
return cleanMessage;
|
|
379
|
+
// If the message already mentions the field label, no need to prefix it.
|
|
380
|
+
const escapedLabel = cleanLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
381
|
+
const labelRegex = new RegExp(`\\b${escapedLabel}\\b`, 'i');
|
|
382
|
+
if (labelRegex.test(cleanMessage))
|
|
383
|
+
return cleanMessage;
|
|
384
|
+
return `${cleanLabel}: ${cleanMessage}`;
|
|
385
|
+
}
|
|
386
|
+
focusFirstInvalidField() {
|
|
387
|
+
const fields = Array.from(document.querySelectorAll('.en__field'));
|
|
388
|
+
for (const field of fields) {
|
|
389
|
+
if (!field.querySelector('.en__field__error'))
|
|
390
|
+
continue;
|
|
391
|
+
const target = field.querySelector('.en__field__element input:not([type="hidden"]), .en__field__element select, .en__field__element textarea');
|
|
392
|
+
if (target && target.offsetParent !== null && !target.hasAttribute('disabled')) {
|
|
393
|
+
target.focus();
|
|
394
|
+
return;
|
|
284
395
|
}
|
|
285
|
-
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
getFieldLabel(field) {
|
|
399
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
400
|
+
const label = (_c = (_b = (_a = field.querySelector('.en__field__label:not(.en__field__label--item)')) !== null && _a !== void 0 ? _a : field.querySelector('.en__field__label')) !== null && _b !== void 0 ? _b : field.querySelector('label')) !== null && _c !== void 0 ? _c : field.querySelector('legend');
|
|
401
|
+
const labelText = (_e = (_d = label === null || label === void 0 ? void 0 : label.textContent) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : '';
|
|
402
|
+
if (labelText)
|
|
403
|
+
return this.normalizeLabel(labelText);
|
|
404
|
+
const input = field.querySelector('input, select, textarea');
|
|
405
|
+
return (_g = (_f = input === null || input === void 0 ? void 0 : input.getAttribute('aria-label')) === null || _f === void 0 ? void 0 : _f.trim()) !== null && _g !== void 0 ? _g : '';
|
|
406
|
+
}
|
|
407
|
+
normalizeLabel(label) {
|
|
408
|
+
return label
|
|
409
|
+
.replace(/\s+/g, ' ')
|
|
410
|
+
.replace(/^[*:\s]+|[*:\s]+$/g, '')
|
|
411
|
+
.trim();
|
|
286
412
|
}
|
|
287
413
|
}
|
|
@@ -3,7 +3,7 @@ export declare class FrequencyUpsellModal extends Modal {
|
|
|
3
3
|
private readonly upsellOptions;
|
|
4
4
|
private _amountWithFees;
|
|
5
5
|
private _upsellAmountWithFees;
|
|
6
|
-
constructor(upsellOptions: FrequencyUpsellOptions);
|
|
6
|
+
constructor(upsellOptions: FrequencyUpsellOptions, onDismiss?: () => void);
|
|
7
7
|
set amountWithFees(value: number);
|
|
8
8
|
set upsellAmountWithFees(value: number);
|
|
9
9
|
updateModalContent(): void;
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ENGrid, Modal } from ".";
|
|
7
7
|
export class FrequencyUpsellModal extends Modal {
|
|
8
|
-
constructor(upsellOptions) {
|
|
8
|
+
constructor(upsellOptions, onDismiss) {
|
|
9
9
|
super({
|
|
10
10
|
onClickOutside: "bounce",
|
|
11
11
|
customClass: `engrid--frequency-upsell-modal ${upsellOptions.customClass}`,
|
|
12
|
-
showCloseX:
|
|
12
|
+
showCloseX: upsellOptions.showCloseX,
|
|
13
|
+
onDismiss,
|
|
13
14
|
});
|
|
14
15
|
this._amountWithFees = 0;
|
|
15
16
|
this._upsellAmountWithFees = 0;
|
|
@@ -31,6 +31,11 @@ export declare class FrequencyUpsell {
|
|
|
31
31
|
*/
|
|
32
32
|
getUpsellAmount(withFee: boolean): number;
|
|
33
33
|
private addEventListeners;
|
|
34
|
+
/**
|
|
35
|
+
* Handle the modal being dismissed via the X button, Esc key, or click-outside.
|
|
36
|
+
* This always counts as a decline (onDecline fires either way).
|
|
37
|
+
*/
|
|
38
|
+
private handleModalDismiss;
|
|
34
39
|
/**
|
|
35
40
|
* Create the frequency field for the upsell, if it does not exist on the page already
|
|
36
41
|
* This is required by DonationFrequency to set the frequency
|
package/dist/frequency-upsell.js
CHANGED
|
@@ -23,7 +23,7 @@ export class FrequencyUpsell {
|
|
|
23
23
|
}
|
|
24
24
|
this.options = this.selectOptions(window.EngridFrequencyUpsell);
|
|
25
25
|
this.logger.log("FrequencyUpsell initialized", this.options);
|
|
26
|
-
this.upsellModal = new FrequencyUpsellModal(this.options);
|
|
26
|
+
this.upsellModal = new FrequencyUpsellModal(this.options, () => this.handleModalDismiss());
|
|
27
27
|
this.createFrequencyField();
|
|
28
28
|
this.addEventListeners();
|
|
29
29
|
}
|
|
@@ -144,6 +144,20 @@ export class FrequencyUpsell {
|
|
|
144
144
|
return true;
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Handle the modal being dismissed via the X button, Esc key, or click-outside.
|
|
149
|
+
* This always counts as a decline (onDecline fires either way).
|
|
150
|
+
*/
|
|
151
|
+
handleModalDismiss() {
|
|
152
|
+
this.logger.log("Frequency upsell modal dismissed (declined)");
|
|
153
|
+
this.options.onDecline();
|
|
154
|
+
if (this.options.submitOnClose) {
|
|
155
|
+
this._form.submitForm();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this._form.dispatchError();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
147
161
|
/**
|
|
148
162
|
* Create the frequency field for the upsell, if it does not exist on the page already
|
|
149
163
|
* This is required by DonationFrequency to set the frequency
|
package/dist/modal.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ interface ModalOptions {
|
|
|
4
4
|
closeButtonLabel?: string;
|
|
5
5
|
customClass?: string;
|
|
6
6
|
showCloseX?: boolean;
|
|
7
|
+
closeOnEsc?: boolean;
|
|
8
|
+
onDismiss?: () => void;
|
|
7
9
|
}
|
|
8
10
|
export declare abstract class Modal {
|
|
9
11
|
modalContent: NodeListOf<Element> | HTMLElement | String;
|
|
@@ -13,7 +15,14 @@ export declare abstract class Modal {
|
|
|
13
15
|
protected constructor(options: ModalOptions);
|
|
14
16
|
private createModal;
|
|
15
17
|
private addEventListeners;
|
|
18
|
+
/**
|
|
19
|
+
* Generic entry point for dismissing the modal.
|
|
20
|
+
* Fires the onDismiss callback before closing, so consumers can react to the modal being
|
|
21
|
+
* dismissed rather than closed via their own explicit button logic.
|
|
22
|
+
*/
|
|
23
|
+
private dismiss;
|
|
16
24
|
private focusTrapHandler;
|
|
25
|
+
private escKeyHandler;
|
|
17
26
|
open(): void;
|
|
18
27
|
close(): void;
|
|
19
28
|
getModalContent(): NodeListOf<Element> | HTMLElement | String;
|
package/dist/modal.js
CHANGED
|
@@ -8,6 +8,8 @@ export class Modal {
|
|
|
8
8
|
closeButtonLabel: "Okay!",
|
|
9
9
|
customClass: "",
|
|
10
10
|
showCloseX: true,
|
|
11
|
+
closeOnEsc: true,
|
|
12
|
+
onDismiss: () => { },
|
|
11
13
|
};
|
|
12
14
|
this.focusTrapHandler = (e) => {
|
|
13
15
|
const modalElement = this.modal;
|
|
@@ -33,6 +35,11 @@ export class Modal {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
};
|
|
38
|
+
this.escKeyHandler = (e) => {
|
|
39
|
+
if (e.key === "Escape" && this.options.closeOnEsc) {
|
|
40
|
+
this.dismiss();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
36
43
|
this.options = Object.assign(Object.assign({}, this.defaultOptions), options);
|
|
37
44
|
this.modalContent = this.getModalContent();
|
|
38
45
|
this.createModal();
|
|
@@ -83,7 +90,7 @@ export class Modal {
|
|
|
83
90
|
button.classList.add("engrid-modal__button");
|
|
84
91
|
button.textContent = this.options.closeButtonLabel;
|
|
85
92
|
button.addEventListener("click", () => {
|
|
86
|
-
this.
|
|
93
|
+
this.dismiss();
|
|
87
94
|
});
|
|
88
95
|
modalBody === null || modalBody === void 0 ? void 0 : modalBody.appendChild(button);
|
|
89
96
|
}
|
|
@@ -93,13 +100,13 @@ export class Modal {
|
|
|
93
100
|
var _a, _b, _c, _d, _e;
|
|
94
101
|
// Close event on top X
|
|
95
102
|
(_b = (_a = this.modal) === null || _a === void 0 ? void 0 : _a.querySelector(".engrid-modal__close")) === null || _b === void 0 ? void 0 : _b.addEventListener("click", () => {
|
|
96
|
-
this.
|
|
103
|
+
this.dismiss();
|
|
97
104
|
});
|
|
98
105
|
// Bounce scale when clicking outside of modal
|
|
99
106
|
(_d = (_c = this.modal) === null || _c === void 0 ? void 0 : _c.querySelector(".engrid-modal__overlay")) === null || _d === void 0 ? void 0 : _d.addEventListener("click", (event) => {
|
|
100
107
|
if (event.target === event.currentTarget) {
|
|
101
108
|
if (this.options.onClickOutside === "close") {
|
|
102
|
-
this.
|
|
109
|
+
this.dismiss();
|
|
103
110
|
}
|
|
104
111
|
else if (this.options.onClickOutside === "bounce") {
|
|
105
112
|
const modal = document.querySelector(".engrid-modal");
|
|
@@ -115,25 +122,37 @@ export class Modal {
|
|
|
115
122
|
const closeEls = (_e = this.modal) === null || _e === void 0 ? void 0 : _e.querySelectorAll(".modal__close");
|
|
116
123
|
closeEls === null || closeEls === void 0 ? void 0 : closeEls.forEach((el) => {
|
|
117
124
|
el.addEventListener("click", () => {
|
|
118
|
-
this.
|
|
125
|
+
this.dismiss();
|
|
119
126
|
});
|
|
120
127
|
});
|
|
121
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Generic entry point for dismissing the modal.
|
|
131
|
+
* Fires the onDismiss callback before closing, so consumers can react to the modal being
|
|
132
|
+
* dismissed rather than closed via their own explicit button logic.
|
|
133
|
+
*/
|
|
134
|
+
dismiss() {
|
|
135
|
+
var _a, _b;
|
|
136
|
+
(_b = (_a = this.options).onDismiss) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
137
|
+
this.close();
|
|
138
|
+
}
|
|
122
139
|
open() {
|
|
123
|
-
var _a, _b, _c, _d;
|
|
140
|
+
var _a, _b, _c, _d, _e;
|
|
124
141
|
ENGrid.setBodyData("has-lightbox", "true");
|
|
125
142
|
(_a = this.modal) === null || _a === void 0 ? void 0 : _a.classList.remove("modal--hidden");
|
|
126
143
|
(_b = this.modal) === null || _b === void 0 ? void 0 : _b.removeAttribute("aria-hidden");
|
|
127
144
|
const container = (_c = this.modal) === null || _c === void 0 ? void 0 : _c.querySelector(".engrid-modal__container");
|
|
128
145
|
container === null || container === void 0 ? void 0 : container.focus({ preventScroll: true });
|
|
129
146
|
(_d = this.modal) === null || _d === void 0 ? void 0 : _d.addEventListener("keydown", this.focusTrapHandler);
|
|
147
|
+
(_e = this.modal) === null || _e === void 0 ? void 0 : _e.addEventListener("keydown", this.escKeyHandler);
|
|
130
148
|
}
|
|
131
149
|
close() {
|
|
132
|
-
var _a, _b, _c;
|
|
150
|
+
var _a, _b, _c, _d;
|
|
133
151
|
ENGrid.setBodyData("has-lightbox", false);
|
|
134
152
|
(_a = this.modal) === null || _a === void 0 ? void 0 : _a.classList.add("modal--hidden");
|
|
135
153
|
(_b = this.modal) === null || _b === void 0 ? void 0 : _b.setAttribute("aria-hidden", "true");
|
|
136
154
|
(_c = this.modal) === null || _c === void 0 ? void 0 : _c.removeEventListener("keydown", this.focusTrapHandler);
|
|
155
|
+
(_d = this.modal) === null || _d === void 0 ? void 0 : _d.removeEventListener("keydown", this.escKeyHandler);
|
|
137
156
|
}
|
|
138
157
|
getModalContent() {
|
|
139
158
|
return "<h1>Default Modal Content</h1>";
|
package/dist/page-background.js
CHANGED
|
@@ -7,6 +7,9 @@ export class PageBackground {
|
|
|
7
7
|
this.bodyBannerImage = null;
|
|
8
8
|
this.mutationObserver = null;
|
|
9
9
|
this.logger = new EngridLogger("PageBackground", "lightblue", "darkblue", "🖼️");
|
|
10
|
+
if (typeof window.UseBodyBannerImageAsBackground !== "undefined") {
|
|
11
|
+
useBodyBannerImage = !!window.UseBodyBannerImageAsBackground;
|
|
12
|
+
}
|
|
10
13
|
if (useBodyBannerImage) {
|
|
11
14
|
this.bodyBannerImage = this.findBodyBannerImage();
|
|
12
15
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AppVersion = "0.27.
|
|
1
|
+
export declare const AppVersion = "0.27.4";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AppVersion = "0.27.
|
|
1
|
+
export const AppVersion = "0.27.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4site/engrid-scripts",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.4",
|
|
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": "e01ad2b4ea0b53edda39388f6597976e33e5f129",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"nodemon": "^2.0.20"
|
|
31
31
|
}
|