Skip to content

Commit e138178

Browse files
authored
Merge pull request #141 from adobecom/dev
T3-26.20 Stage Release
2 parents 2506d76 + fae54be commit e138178

18 files changed

Lines changed: 895 additions & 176 deletions

File tree

.claude/commands/pr-review.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
Review the changes on the current branch against `dev` for correctness, vanilla JS best practices, and performance. Focus on what the code does, whether it does it efficiently, and flag anything that would hurt the reader or the runtime.
2+
3+
## 1. Collect the diff
4+
5+
```bash
6+
git diff dev...HEAD
7+
```
8+
9+
Read every changed source file in full (not just the diff lines) so you have complete context.
10+
11+
## 2. For each changed file, produce a section
12+
13+
Use this structure per file:
14+
15+
### `path/to/file.js`
16+
17+
**What changed** — one paragraph. Describe the intent of the change in plain English (e.g. "Adds a helper that strips HTML tags and decodes entities before using a string as modal ARIA label text, so screen readers receive clean text instead of raw markup.").
18+
19+
**Concrete example** — show a before/after pair that makes the change tangible:
20+
21+
```
22+
Before: aria-label="<strong>VP, Engineering</strong>"
23+
After: aria-label="VP, Engineering"
24+
```
25+
26+
**Issues found** — one bullet per issue, with severity (`critical` / `warn` / `nit`) and the exact line or pattern:
27+
28+
- `[warn] line 42` — description + a corrected snippet
29+
30+
If there are no issues, write `No issues found.`
31+
32+
## 3. Review criteria
33+
34+
Apply every check below. Quote the offending code for any failure.
35+
36+
### Correctness
37+
- Does the logic handle `null`, `undefined`, and empty strings without throwing?
38+
- Are all edge cases covered (empty arrays, missing DOM nodes, async races)?
39+
- Are regular expressions anchored or guarded correctly — no catastrophic backtracking, no runaway `.+` on untrusted input?
40+
41+
### Vanilla JS best practices
42+
- No unnecessary `innerHTML` assignments where `textContent` suffices (avoids XSS, faster parse)
43+
- Prefer `el.textContent = value` over `el.innerHTML = value` when the value is plain text
44+
- Use `document.createDocumentFragment()` or a single `append()` call when inserting multiple nodes — avoid repeated DOM mutations in a loop
45+
- Use `const` / `let`; never `var`
46+
- Arrow functions for closures; named `function` declarations for top-level exports
47+
- Destructuring and default parameters where they simplify without obscuring
48+
- No `==` — use `===` for all comparisons
49+
- Avoid `try/catch` inside hot loops; guard with a conditional check before entering
50+
- No dead code, no commented-out blocks left in
51+
52+
### Performance & optimisation
53+
- **DOM reads/writes**: never interleave reads and writes in a loop (causes layout thrashing). Batch all reads, then all writes.
54+
- **Regex**: compile once at module scope (`const RE = /pattern/`) rather than inside a called function if the regex is invariant
55+
- **String building**: prefer a single `replace` chain on a string rather than splitting into an array, mapping, and rejoining when the operation is purely substitution
56+
- **Event listeners**: confirm `removeEventListener` or `AbortController` is used if listeners are attached inside `init()` — leaks on re-init
57+
- **Selector cost**: `querySelector` is fine; avoid `querySelectorAll` + `forEach` when a targeted `querySelector` on a known container is possible
58+
- **Lazy work**: heavy computation (entity decoding, full DOM traversal) should be guarded so it only runs when the input actually needs it — not on every call
59+
60+
### Code size & readability
61+
- No abstraction that is only used once and is no simpler than its inline equivalent
62+
- Helper names should say *what* they return, not *how* (e.g. `plainText(title)` not `processAndCleanTitle`)
63+
- Functions longer than ~30 lines should be split unless the logic is a single clear pipeline
64+
- No multi-line comment blocks; inline comments only for non-obvious invariants
65+
66+
## 4. Summary table
67+
68+
End with a compact table:
69+
70+
| File | Critical | Warn | Nit |
71+
|------|----------|------|-----|
72+
| path/to/file.js | 0 | 2 | 1 |
73+
74+
If all counts are 0, say **"Ready to merge — no issues found."**
75+
If any `critical` issues exist, say **"Block on critical issues before merging."**
76+
If only `warn`/`nit`, say **"Good to merge after addressing warnings."**

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Project-specific slash commands:
138138
- `/find-util <description>` — search utils/ and features/ for existing code before writing new code
139139
- `/coverage-gaps` — inventory every block/utility and flag missing or shallow tests
140140
- `/pr-ready` — lint + test + changed-file review + commit message check before opening a PR
141+
- `/pr-review` — review branch changes for correctness, JS best practices, and performance; produces a per-file report with before/after examples and a severity summary table
141142

142143
### Built-in skills
143144

event-libs/v1/blocks/event-agenda/event-agenda.css

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737

3838
.event-agenda .agenda-list-item .agenda-time {
3939
font-weight: 700;
40-
min-width: 75px;
40+
min-width: 0;
4141
font-size: 18px;
4242
line-height: 150%; /* 27px */
4343
padding: 0 16px;
44+
align-self: start;
4445
}
4546

4647
.event-agenda .agenda-item-container {
@@ -53,30 +54,22 @@
5354
.event-agenda .agenda-item-container .agenda-list-item {
5455
position: relative;
5556
border-radius: 5px;
56-
display: flex;
57-
flex-direction: row;
57+
display: grid;
58+
grid-template-columns: subgrid;
59+
grid-column: 1 / -1;
5860
align-items: stretch;
5961
align-self: stretch;
6062
}
6163

62-
.event-agenda .agenda-list-item .agenda-title-detail-container::before {
63-
content: "";
64-
display: block;
65-
position: absolute;
66-
top: 0;
67-
left: 0;
68-
height: 100%;
69-
width: 1px;
70-
background: #D5D0D0;
71-
}
72-
73-
.event-agenda.collapsible .agenda-list-item .agenda-title-detail-container::before {
74-
display: none;
75-
}
76-
7764
.event-agenda .agenda-item-container .agenda-list-item .agenda-title-detail-container {
7865
position: relative;
7966
padding: 0 16px;
67+
border-inline-start: 1px solid #D5D0D0;
68+
min-width: 0;
69+
}
70+
71+
.event-agenda.collapsible .agenda-item-container .agenda-list-item .agenda-title-detail-container {
72+
border-inline-start: none;
8073
}
8174

8275
.event-agenda .agenda-item-container .agenda-list-item .agenda-title-detail {
@@ -129,10 +122,11 @@
129122
}
130123

131124
.event-agenda .agenda-container .agenda-item-container .column {
132-
display: flex;
133-
flex-direction: column;
134-
gap: 32px;
125+
display: grid;
126+
grid-template-columns: minmax(75px, max-content) 1fr;
127+
row-gap: 32px;
135128
flex: 100%;
129+
min-width: 0;
136130
}
137131

138132
/* Background variant */
@@ -143,21 +137,21 @@
143137

144138
.event-agenda.background .agenda-item-container .agenda-list-item .agenda-title-detail-container {
145139
padding: 20px 16px;
146-
flex-grow: 1;
140+
box-sizing: border-box;
147141
background-color: #fff;
148142
display: flex;
143+
align-self: stretch;
149144
}
150145

151146
/* Collapsible variant */
152147
.event-agenda.collapsible .agenda-item-container .column {
153-
gap: 16px;
148+
grid-template-columns: 1fr;
149+
row-gap: 16px;
154150
}
155151

156152
.event-agenda.collapsible .agenda-item-container .agenda-list-item {
157153
width: 100%;
158154
box-sizing: border-box;
159-
display: flex;
160-
flex-direction: column;
161155
}
162156

163157
.event-agenda.collapsible .column:only-child .agenda-list-item:only-child,
@@ -174,6 +168,7 @@
174168

175169
.event-agenda.background .agenda-item-container .agenda-list-item .agenda-time {
176170
background-color: #fff;
171+
align-self: stretch;
177172
}
178173

179174
.event-agenda.collapsible .agenda-list-item.expandable {
@@ -242,14 +237,18 @@
242237
padding: 40px 56px 40px calc((100% - var(--grid-container-width) - 80px) / 2);
243238
}
244239

245-
.event-agenda.collapsible .agenda-item-container .agenda-list-item {
246-
flex-direction: row;
240+
.event-agenda.collapsible .agenda-item-container .column {
241+
grid-template-columns: minmax(215px, max-content) 1fr;
247242
}
248243

249244
.event-agenda.collapsible .agenda-item-container .agenda-list-item .agenda-time {
250245
padding: 20px 16px;
251246
}
252247

248+
.event-agenda.collapsible .agenda-item-container .agenda-list-item .agenda-title-detail-container {
249+
border-inline-start: 1px solid #D5D0D0;
250+
}
251+
253252
.event-agenda.background .agenda-item-container .agenda-list-item .agenda-time {
254253
height: 100%;
255254
box-sizing: border-box;
@@ -258,14 +257,6 @@
258257
.event-agenda.collapsible.background .agenda-item-container .agenda-list-item.expanded .agenda-time {
259258
background: linear-gradient(180deg, #FAFAFA 22.33%, rgb(255 255 255 / 0%) 99.96%);
260259
}
261-
262-
.event-agenda.collapsible .agenda-list-item .agenda-title-detail-container::before {
263-
display: block;
264-
top: -20px;
265-
left: 0;
266-
height: calc(100% + 40px);
267-
}
268-
269260
}
270261

271262
@media (min-width: 1440px) {

event-libs/v1/blocks/events-form/events-form.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { deleteAttendeeFromEvent, getAndCreateAndAddAttendee, getAttendee, getEv
22
import BlockMediator from '../../deps/block-mediator.min.js';
33
import { signIn, decorateEvent } from '../../utils/decorate.js';
44
import { dictionaryManager, getInviteOnlyNoCampaignMessage } from '../../utils/dictionary-manager.js';
5-
import { getEventConfig, LIBS, getMetadata, getSusiOptions, getValidCampaignIdFromUrl } from '../../utils/utils.js';
5+
import {
6+
getEventConfig, LIBS, getMetadata, getSusiOptions, getValidCampaignIdFromUrl,
7+
} from '../../utils/utils.js';
68
import { FALLBACK_LOCALES, CAMPAIGN_ID_PATTERN } from '../../utils/constances.js';
79
import { BASE_ATTENDEE_DATA_FILTER } from '../../utils/data-utils.js';
10+
import { parseRsvpFieldLimit, stripTags } from '../../utils/sanitize-utils.js';
11+
import { applyImplicitContactMethodsToPayload, getImplicitConsentRaw } from '../../utils/rsvp-consent.js';
812

913
const eventConfig = getEventConfig();
1014
const miloLibs = eventConfig?.miloConfig?.miloLibs ? eventConfig.miloConfig.miloLibs : LIBS;
@@ -209,6 +213,8 @@ async function submitForm(bp) {
209213
}
210214
}
211215

216+
applyImplicitContactMethodsToPayload(form, payload);
217+
212218
const isValid = Object.keys(payload).reduce((valid, key) => {
213219
const field = form.querySelector(`[data-field-id=${key}]`);
214220

@@ -228,7 +234,7 @@ async function submitForm(bp) {
228234
}
229235

230236
if (sanitizeList.includes(key)) {
231-
payload[key] = sanitizeComment(payload[key]);
237+
payload[key] = sanitizeComment(stripTags(payload[key]));
232238
}
233239

234240
return valid;
@@ -438,7 +444,9 @@ function createHeading({ label }, el) {
438444
return createTag(el, {}, dictionaryManager.getValue(label, 'rsvp-fields'));
439445
}
440446

441-
function createInput({ type, field, placeholder, required, defval, pattern, title }) {
447+
function createInput({
448+
type, field, placeholder, required, defval, pattern, title, limit,
449+
}) {
442450
const placeholderText = placeholder ? dictionaryManager.getValue(placeholder, 'rsvp-fields') : '';
443451
const isPhoneField = type === 'tel' || type === 'phone' || (typeof field === 'string' && PHONE_FIELD_RE.test(field));
444452
const attrs = { type: isPhoneField ? 'tel' : type, id: field, placeholder: placeholderText, value: defval };
@@ -449,14 +457,17 @@ function createInput({ type, field, placeholder, required, defval, pattern, titl
449457
const resolvedPattern = pattern || (isPhoneField ? PHONE_PATTERN : null);
450458
if (resolvedPattern) attrs.pattern = resolvedPattern;
451459
if (title) attrs.title = title;
460+
if (limit != null) attrs.maxlength = limit;
452461
const input = createTag('input', attrs);
453462
if (required === 'x') input.setAttribute('required', 'required');
454463
return input;
455464
}
456465

457-
function createTextArea({ field, placeholder, required, defval }) {
466+
function createTextArea({ field, placeholder, required, defval, limit }) {
458467
const placeholderText = placeholder ? dictionaryManager.getValue(placeholder, 'rsvp-fields') : '';
459-
const input = createTag('textarea', { id: field, placeholder: placeholderText, value: defval });
468+
const attrs = { id: field, placeholder: placeholderText, value: defval };
469+
if (limit != null) attrs.maxlength = limit;
470+
const input = createTag('textarea', attrs);
460471
if (required === 'x') input.setAttribute('required', 'required');
461472
return input;
462473
}
@@ -594,6 +605,14 @@ async function loadConsent(form, consentData) {
594605
await loadFragment(termsFragLink);
595606

596607
termsWrapper.classList.remove('transparent');
608+
609+
const implicitRaw = getImplicitConsentRaw(termsWrapper, consentData);
610+
if (implicitRaw) {
611+
termsWrapper.dataset.implicitConsent = implicitRaw;
612+
} else {
613+
delete termsWrapper.dataset.implicitConsent;
614+
}
615+
597616
const uls = termsWrapper.querySelectorAll('ul');
598617

599618
const defval = '';
@@ -871,6 +890,7 @@ async function createForm(bp, formData) {
871890

872891
if (rsvpFieldsData) {
873892
const { required, visible } = rsvpFieldsData;
893+
// Sheet-backed fields (e.g. requiresTicket) must appear in `visible` or they are dropped before render.
874894
json.data = json.data
875895
.map((obj) => {
876896
const lowkey = lowercaseKeys(obj);
@@ -913,7 +933,8 @@ async function createForm(bp, formData) {
913933

914934
json.data.forEach(async (fd) => {
915935
fd.type = fd.type || 'text';
916-
if (fd.type === 'text') sanitizeList.push(fd.field);
936+
fd.limit = parseRsvpFieldLimit(fd.limit);
937+
if (fd.type === 'text' || fd.type === 'text-area') sanitizeList.push(fd.field);
917938
const style = fd.extra ? ` events-form-${fd.extra}` : '';
918939
const fieldWrapper = createTag(
919940
'div',

event-libs/v1/blocks/profile-cards/profile-cards.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@
247247
padding: 32px;
248248
align-items: flex-start;
249249
}
250+
251+
.profile-cards-modal .profile-cards-modal-image .card-image-container img {
252+
min-width: 385px;
253+
}
250254
}
251255

252256
/* Desktop and up */

event-libs/v1/blocks/profile-cards/profile-cards.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,52 @@ function appendBio(contentContainer, bio) {
228228
contentContainer.append(description);
229229
}
230230

231+
function decodeHtmlEntities(str) {
232+
return str
233+
.replace(/&#x([0-9a-f]{1,6});?/gi, (full, hex) => {
234+
const cp = parseInt(hex, 16);
235+
if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF) return full;
236+
try {
237+
return String.fromCodePoint(cp);
238+
} catch {
239+
return full;
240+
}
241+
})
242+
.replace(/&#(\d{1,7});?/g, (full, dec) => {
243+
const cp = parseInt(dec, 10);
244+
if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF) return full;
245+
try {
246+
return String.fromCodePoint(cp);
247+
} catch {
248+
return full;
249+
}
250+
})
251+
.replace(/&nbsp;/gi, ' ')
252+
.replace(/&quot;/g, '"')
253+
.replace(/&apos;/g, "'")
254+
.replace(/&#39;/g, "'")
255+
.replace(/&lt;/g, '<')
256+
.replace(/&gt;/g, '>')
257+
.replace(/&amp;/g, '&');
258+
}
259+
260+
function escapeHtmlPcdata(text) {
261+
return text
262+
.replace(/&/g, '&amp;')
263+
.replace(/</g, '&lt;')
264+
.replace(/>/g, '&gt;');
265+
}
266+
267+
function modalJobTitlePlainText(title) {
268+
if (title == null || typeof title !== 'string') return '';
269+
const trimmed = title.trim();
270+
if (!trimmed) return '';
271+
const withoutTags = trimmed.includes('<')
272+
? trimmed.replace(/<[^>]+>/g, ' ')
273+
: trimmed;
274+
return decodeHtmlEntities(withoutTags).replace(/\s+/g, ' ').trim();
275+
}
276+
231277
function getModalId(data, index) {
232278
const fullName = getProfileName(data);
233279
const slug = fullName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
@@ -253,7 +299,7 @@ export async function buildModalContent(profileData) {
253299
const textContainer = createTag('div', { class: 'profile-cards-modal-text' });
254300
const imageContainer = createTag('div', { class: 'profile-cards-modal-image' });
255301
const fullName = getProfileName(profileData);
256-
const title = createTag('p', { class: 'card-title' }, profileData?.title || '');
302+
const title = createTag('p', { class: 'card-title' }, escapeHtmlPcdata(modalJobTitlePlainText(profileData?.title)));
257303
const name = createTag('h2', { class: 'card-name', tabindex: '0' }, fullName);
258304

259305
textContainer.append(title, name);

0 commit comments

Comments
 (0)