Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container PR for initial TWP features #2407

Merged
merged 31 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
39a27ea
Mwpw-136871: TwP merch card (#2088)
Axelcureno Apr 3, 2024
0b4e397
TWP block draft (#2105)
3ch023 Apr 5, 2024
4491118
Mwpw 136871: Price display inside TwP merch card (#2110)
Axelcureno Apr 9, 2024
dc7e023
Mwpw 136871 (#2154)
Axelcureno Apr 17, 2024
ed261cd
MWPW-138927: merch-twp WIP (#2160)
yesil Apr 18, 2024
8b2b97a
MWPW-138927: desktop step 2 layout WIP
yesil Apr 22, 2024
2971caa
remove comment
yesil Apr 22, 2024
bf04e5d
pr feedback
yesil Apr 22, 2024
cf77779
update deps
yesil May 21, 2024
5e54633
update deps
yesil May 21, 2024
3d11396
update deps
yesil May 21, 2024
b2f8be9
update deps
yesil May 21, 2024
b5a3c8f
fixed tests
yesil May 21, 2024
2058351
fix lagging subscription panel
yesil May 21, 2024
c58efb0
Merge remote-tracking branch 'upstream/stage' into twp
yesil May 21, 2024
ce11dd2
Merge remote-tracking branch 'upstream/stage' into twp
yesil May 31, 2024
e6afe56
WIP
yesil May 31, 2024
f0891c4
fix tests
yesil May 31, 2024
04ead20
fix tests
yesil Jun 3, 2024
16cb1bb
wip
yesil Jun 3, 2024
9d18c9a
fix failing test
yesil Jun 3, 2024
56b3a43
fix failing tests
yesil Jun 3, 2024
7f691fe
remove files
yesil Jun 3, 2024
cc28d9e
Merge branch 'stage' into twp
yesil Jun 3, 2024
16ab911
improve code coverage
yesil Jun 3, 2024
cd59186
Merge branch 'stage' into twp
3ch023 Jun 7, 2024
52c214b
address first batch of comments
3ch023 Jun 7, 2024
c6460b0
fix dependencies
3ch023 Jun 9, 2024
3285bd0
fix unit tests
3ch023 Jun 9, 2024
91c148e
add missing test cases
3ch023 Jun 14, 2024
e199f2d
Merge branch 'stage' into twp
3ch023 Jun 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libs/blocks/merch-card/merch-card.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ div[class*="-merch-card"] > div,
color: var(--text-color);
}

.twp.merch-card .merch-card-price {
font-weight: 700;
margin: 0;
}

merch-card.special-offers del span[is="inline-price"] {
text-decoration: line-through;
}
Expand Down
187 changes: 119 additions & 68 deletions libs/blocks/merch-card/merch-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@ import '../../deps/merch-card.js';

const TAG_PATTERN = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-].*$/;

const CARD_TYPES = ['segment', 'special-offers', 'plans', 'catalog', 'product', 'inline-heading', 'image', 'mini-compare-chart'];
const SEGMENT = 'segment';
const SPECIAL_OFFERS = 'special-offers';
const PLANS = 'plans';
const CATALOG = 'catalog';
const PRODUCT = 'product';
const MINI_COMPARE_CHART = 'mini-compare-chart';
const TWP = 'twp';
const CARD_TYPES = [
SEGMENT,
SPECIAL_OFFERS,
PLANS,
CATALOG,
PRODUCT,
'inline-heading',
'image',
MINI_COMPARE_CHART,
TWP,
];

const CARD_SIZES = ['wide', 'super-wide'];

Expand All @@ -25,13 +42,9 @@ const HEADING_MAP = {
},
};

const MINI_COMPARE_CHART = 'mini-compare-chart';
const PLANS = 'plans';
const SEGMENT = 'segment';

const INNER_ELEMENTS_SELECTOR = 'h2, h3, h4, h5, p, ul, em';

const MULTI_OFFER_CARDS = ['plans', 'product', MINI_COMPARE_CHART];
const MULTI_OFFER_CARDS = [PLANS, PRODUCT, MINI_COMPARE_CHART, TWP];
// Force cards to refresh once they become visible so that the footer rows are properly aligned.
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
Expand All @@ -46,10 +59,10 @@ const getPodType = (styles) => styles?.find((style) => CARD_TYPES.includes(style
const isHeadingTag = (tagName) => /^H[2-5]$/.test(tagName);
const isParagraphTag = (tagName) => tagName === 'P';

const appendSlot = (slotEls, slotName, merchCard) => {
const appendSlot = (slotEls, slotName, merchCard, nodeName = 'p') => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference, but 3+ parameters become difficult to manage, especially if one needs to be omitted at some point. A config object might be better for maintenance.

if (slotEls.length === 0 || merchCard.variant !== MINI_COMPARE_CHART) return;
const newEl = createTag(
'p',
nodeName,
{ slot: slotName, class: slotName },
);
slotEls.forEach((e) => {
Expand All @@ -72,6 +85,68 @@ export async function loadMnemonicList(foreground) {
}
}

function extractQuantitySelect(el) {
const quantitySelectConfig = [...el.querySelectorAll('ul')]
.find((ul) => ul.querySelector('li')?.innerText?.includes('Quantity'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includes('Quantity') might create some issues if the content lowercase. Would we need to make the string lowercase first and then check if the string is included ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does sound a bit error-prone and might lead to confusion, is there any other mechanism to stop this from occurring?

Copy link
Contributor

@3ch023 3ch023 Jun 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might create some issues if the content lowercase.

I am not sure we should support the lowercase option. In general, it's a bad idea to depend on the text since it could get translated.. and break down in non-english geos.
I logged a jira to address it, there should be no logic depending on text:
https://jira.corp.adobe.com/browse/MWPW-152072

This will be addressed in the separate PR if you are ok with it. Need to design and agree with GWP on how it will work in future.
thx a lot for bringing attention to the issue here.

const configMarkup = quantitySelectConfig?.querySelector('ul');
if (!configMarkup) return null;
const config = configMarkup.children;
if (config.length !== 2) return null;
const attributes = {};
attributes.title = config[0].textContent.trim();
const values = config[1].textContent.split(',')
.map((value) => value.trim())
.filter((value) => /^\d*$/.test(value))
.map((value) => (value === '' ? undefined : Number(value)));
yesil marked this conversation as resolved.
Show resolved Hide resolved
quantitySelectConfig.remove();
if (![3, 4, 5].includes(values.length)) return null;
import('../../deps/merch-quantity-select.js');
[attributes.min, attributes.max, attributes.step, attributes['default-value'], attributes['max-input']] = values;
const quantitySelect = createTag('merch-quantity-select', attributes);
return quantitySelect;
}

const parseTwpContent = async (el, merchCard) => {
const quantitySelect = extractQuantitySelect(el);
if (quantitySelect) {
merchCard.append(quantitySelect);
}
let allElements = el?.children[0]?.children[0]?.children;
if (!allElements?.length) return;
allElements = [...allElements];
const contentGroups = allElements.reduce((acc, curr) => {
if (curr.tagName.toLowerCase() === 'p' && curr.textContent.trim() === '--') {
acc.push([]);
} else {
acc[acc.length - 1].push(curr);
}
return acc;
}, [[]]);

contentGroups.forEach((group, index) => {
if (index === 0) { // Top section
const headings = group.filter((e) => e.tagName.toLowerCase() === 'h3');
const topBody = group.filter((e) => e.tagName.toLowerCase() === 'p');
appendSlot(headings, 'heading-xs', merchCard);
appendSlot(topBody, 'body-xs-top', merchCard);
} else if (index === 1) { // Body section
const content = group.filter((e) => e.tagName.toLowerCase() === 'p' || e.tagName.toLowerCase() === 'ul');
const bodySlot = createTag('div', { slot: 'body-xs' }, content);
merchCard.append(bodySlot);
} else if (index === 2) { // Footer section
const footerContent = group.filter((e) => ['h5', 'p'].includes(e.tagName.toLowerCase()));
const footer = createTag('div', { slot: 'footer' }, footerContent);
merchCard.append(footer);
}
});

const offerSelection = el.querySelector('ul');
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
await initOfferSelection(merchCard, offerSelection);
}
};

const parseContent = async (el, merchCard) => {
let bodySlotName = `body-${merchCard.variant !== MINI_COMPARE_CHART ? 'xs' : 'm'}`;
let headingMCount = 0;
Expand Down Expand Up @@ -201,7 +276,7 @@ const decorateMerchCardLinkAnalytics = (el) => {
};

const addStock = (merchCard, styles) => {
if (styles.includes('add-stock')) {
if (styles.includes('add-stock') && merchCard.variant !== TWP) {
let stock;
const selector = styles.includes('edu') ? '.merch-offers.stock.edu > *' : '.merch-offers.stock > *';
const [label, ...rest] = [...document.querySelectorAll(selector)];
Expand All @@ -225,27 +300,6 @@ const simplifyHrs = (el) => {
});
};

async function extractQuantitySelect(el) {
const quantitySelectConfig = el.querySelector('ul');
if (!quantitySelectConfig) return null;
const configMarkup = quantitySelectConfig.querySelector('li');
if (!configMarkup || !configMarkup.textContent.includes('Quantity')) return null;
const config = configMarkup.querySelector('ul').querySelectorAll('li');
if (config.length !== 2) return null;
const attributes = {};
attributes.title = config[0].textContent.trim();
const values = config[1].textContent.split(',')
.map((value) => value.trim())
.filter((value) => /^\d*$/.test(value))
.map((value) => (value === '' ? undefined : Number(value)));
if (![3, 4, 5].includes(values.length)) return null;
await import('../../deps/merch-quantity-select.js');
[attributes.min, attributes.max, attributes.step, attributes['default-value'], attributes['max-input']] = values;
const quantitySelect = createTag('merch-quantity-select', attributes);
quantitySelectConfig.remove();
return quantitySelect;
}

const getMiniCompareChartFooterRows = (el) => {
let footerRows = Array.from(el.children).slice(1);
footerRows = footerRows.filter((row) => !row.querySelector('.footer-row-cell'));
Expand Down Expand Up @@ -289,7 +343,7 @@ const setMiniCompareOfferSlot = (merchCard, offers) => {
export default async function init(el) {
if (!el.querySelector(INNER_ELEMENTS_SELECTOR)) return el;
const styles = [...el.classList];
const cardType = getPodType(styles) || 'product';
const cardType = getPodType(styles) || PRODUCT;
if (!styles.includes(cardType)) {
styles.push(cardType);
}
Expand Down Expand Up @@ -344,7 +398,6 @@ export default async function init(el) {
);
merchCard.setAttribute('badge-color', badge.badgeColor);
merchCard.setAttribute('badge-text', badge.badgeText);
if (document.querySelector('html').dir === 'rtl') merchCard.setAttribute('is-rtl', 'true');
merchCard.classList.add('badge-card');
}
}
Expand Down Expand Up @@ -372,7 +425,7 @@ export default async function init(el) {
}
}
});
const actionMenuContent = cardType === 'catalog'
const actionMenuContent = cardType === CATALOG
? getActionMenuContent(el)
: null;
if (actionMenuContent) {
Expand All @@ -397,7 +450,6 @@ export default async function init(el) {
imageSlot.appendChild(image);
merchCard.appendChild(imageSlot);
}
parseContent(el, merchCard);
if (!icons || icons.length > 0) {
const iconImgs = Array.from(icons).map((icon) => {
const img = {
Expand All @@ -421,46 +473,45 @@ export default async function init(el) {
merchCard.setAttribute('filters', categories.join(','));
merchCard.setAttribute('types', types.join(','));

const footer = createTag('div', { slot: 'footer' });
if (ctas) {
if (merchCard.variant === 'mini-compare-chart') {
decorateButtons(ctas, 'button-l');
} else {
decorateButtons(ctas);
}
const links = ctas.querySelectorAll('a');
ctas.remove();
footer.append(...links);
}
merchCard.appendChild(footer);
if (merchCard.variant !== TWP) {
parseContent(el, merchCard);

if (MULTI_OFFER_CARDS.includes(cardType)) {
if (merchCard.variant === MINI_COMPARE_CHART) {
const miniCompareOffers = createTag('div', { slot: 'offers' });
merchCard.append(miniCompareOffers);
}
const quantitySelect = await extractQuantitySelect(el, cardType);
const offerSelection = el.querySelector('ul');
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
setMiniCompareOfferSlot(merchCard, undefined);
initOfferSelection(merchCard, offerSelection, quantitySelect);
const footer = createTag('div', { slot: 'footer' });
if (ctas) {
decorateButtons(ctas, (merchCard.variant === MINI_COMPARE_CHART) ? 'button-l' : undefined);
footer.append(ctas);
}
if (quantitySelect) {
merchCard.appendChild(footer);

if (MULTI_OFFER_CARDS.includes(cardType)) {
const quantitySelect = extractQuantitySelect(el);
const offerSelection = el.querySelector('ul');
if (merchCard.variant === MINI_COMPARE_CHART) {
setMiniCompareOfferSlot(merchCard, quantitySelect);
} else {
const bodySlot = merchCard.querySelector('div[slot="body-xs"]');
bodySlot.append(quantitySelect);
const miniCompareOffers = createTag('div', { slot: 'offers' });
merchCard.append(miniCompareOffers);
}
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
setMiniCompareOfferSlot(merchCard, undefined);
initOfferSelection(merchCard, offerSelection, quantitySelect);
}
if (quantitySelect) {
if (merchCard.variant === MINI_COMPARE_CHART) {
setMiniCompareOfferSlot(merchCard, quantitySelect);
} else {
const bodySlot = merchCard.querySelector('div[slot="body-xs"]');
bodySlot.append(quantitySelect);
}
}
}

decorateBlockHrs(merchCard);
simplifyHrs(merchCard);
if (merchCard.classList.contains('has-divider')) merchCard.setAttribute('custom-hr', true);
decorateFooterRows(merchCard, footerRows);
} else {
parseTwpContent(el, merchCard);
}
decorateBlockHrs(merchCard);
simplifyHrs(merchCard);
if (merchCard.classList.contains('has-divider')) {
merchCard.setAttribute('custom-hr', true);
}
decorateFooterRows(merchCard, footerRows);
el.replaceWith(merchCard);
decorateMerchCardLinkAnalytics(merchCard);
return merchCard;
Expand Down
57 changes: 34 additions & 23 deletions libs/blocks/merch-card/merch-offer-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,29 @@ import { createTag } from '../../utils/utils.js';
import { decorateButtons } from '../../utils/decorate.js';
import '../../deps/merch-offer-select.js';

const TWP = 'twp';
const MINI_COMPARE_CHART = 'mini-compare-chart';

function createDynamicSlots(el, bodySlot) {
const isTWP = el.variant === TWP;
const pricePlaceholder = el.querySelector("span[is='inline-price']");
if (pricePlaceholder) {
pricePlaceholder.setAttribute('slot', 'price');
} else {
const priceSlot = createTag('h5', { class: 'merch-card-price' });
const tagName = isTWP ? 'p' : 'h5';
const priceSlot = createTag(tagName, { class: 'merch-card-price' });
createTag('span', { slot: 'price', is: 'inline-price' }, null, { parent: priceSlot });
bodySlot.append(priceSlot);
}
if (isTWP) return; // twp card do not display cta's
const p = createTag('p', { class: 'action-area' });
createTag('a', { slot: 'secondary-cta', is: 'checkout-link' }, null, { parent: p });
createTag('a', { slot: 'cta', is: 'checkout-link' }, null, { parent: p });
const footer = el.querySelector('div[slot="footer"]');
footer.append(p);
bodySlot.querySelector('p')?.setAttribute('slot', 'description');
if (el.variant === MINI_COMPARE_CHART) {
const description = el.querySelector('div[slot="body-m"] p:last-child');
if (description) {
const descriptionSlot = el.querySelector('p[slot="description"]');
if (descriptionSlot) {
descriptionSlot.innerHTML += description.innerHTML;
}
}
}
const descriptionSlot = bodySlot.querySelector('p');
if (!descriptionSlot) return;
descriptionSlot.setAttribute('slot', 'description');
}

function createMerchOffer(option, quantitySelector, variant) {
Expand Down Expand Up @@ -57,18 +54,32 @@ function createMerchOffer(option, quantitySelector, variant) {
const isHorizontal = (offerSelection) => [...offerSelection.querySelectorAll('merch-offer')].map((o) => o.text).every((t) => /^\d+.B$/.test(t));

export const initOfferSelection = (merchCard, offerSelection, quantitySelector) => {
const bodySlot = merchCard.querySelector(`div[slot="${merchCard.variant === 'mini-compare-chart' ? 'offers' : 'body-xs'}"]`);
if (!bodySlot) return;
createDynamicSlots(merchCard, bodySlot);
const merchOffers = createTag('merch-offer-select', { container: 'merch-card' });
let merchOfferSlot;
switch (merchCard.variant) {
case 'mini-compare-chart':
merchOfferSlot = merchCard.querySelector('div[slot="body-m"]');
break;
case 'twp':
merchOfferSlot = merchCard.querySelector('[slot="footer"]');
break;
default:
merchOfferSlot = merchCard.querySelector('div[slot="body-xs"]');
break;
}
if (!merchOfferSlot) return;
createDynamicSlots(merchCard, merchOfferSlot);
const merchOfferSelect = createTag('merch-offer-select', { container: 'merch-card' });
if (merchCard.classList.contains('add-stock')) {
merchOfferSelect.setAttribute('stock', '');
}
[...offerSelection.children].forEach((option) => {
merchOffers.append(createMerchOffer(option, quantitySelector, merchCard.variant));
merchOfferSelect.append(createMerchOffer(option, quantitySelector, merchCard.variant));
});
merchOffers.querySelectorAll('a[is="checkout-link"]').forEach((link) => { link.setAttribute('slot', 'cta'); });
if (isHorizontal(merchOffers)) {
merchOffers.setAttribute('variant', 'horizontal');
merchOfferSelect.querySelectorAll('a[is="checkout-link"]').forEach((link) => { link.setAttribute('slot', 'cta'); });
if (isHorizontal(merchOfferSelect)) {
merchOfferSelect.setAttribute('variant', 'horizontal');
}
merchOffers.querySelectorAll('merch-offer').forEach((offer) => {
merchOfferSelect.querySelectorAll('merch-offer').forEach((offer) => {
const links = offer.querySelectorAll('a[is="checkout-link"]');
if (links.length > 1) {
links[0].setAttribute('slot', 'secondary-cta');
Expand All @@ -77,11 +88,11 @@ export const initOfferSelection = (merchCard, offerSelection, quantitySelector)
links[0].setAttribute('slot', 'cta');
}
});
merchOffers.querySelectorAll('span[is="inline-price"]').forEach((price) => { price.setAttribute('slot', 'price'); });
merchOfferSelect.querySelectorAll('span[is="inline-price"]').forEach((price) => { price.setAttribute('slot', 'price'); });
if (quantitySelector) {
quantitySelector.append(merchOffers);
quantitySelector.append(merchOfferSelect);
} else {
bodySlot.append(merchOffers);
merchOfferSlot.append(merchOfferSelect);
}
};

Expand Down
Loading
Loading