diff --git a/creativecloud/blocks/interactive-metadata/interactive-metadata.css b/creativecloud/blocks/interactive-metadata/interactive-metadata.css index 1d892b469..6720397fc 100644 --- a/creativecloud/blocks/interactive-metadata/interactive-metadata.css +++ b/creativecloud/blocks/interactive-metadata/interactive-metadata.css @@ -277,6 +277,10 @@ order: 1; } + .marquee.large.interactive-enabled.content-top-mobile .foreground { + gap: 0; + } + .aside.interactive-enabled .foreground.container .image { display: block; width: 100%; @@ -407,6 +411,10 @@ .marquee.large.interactive-enabled.content-top-tablet .text { order: 1; } + + .marquee.large.interactive-enabled.content-top-tablet .foreground { + gap: 0; + } } @media screen and (max-width: 1200px) { diff --git a/creativecloud/blocks/nonprofit/constants.js b/creativecloud/blocks/nonprofit/constants.js new file mode 100644 index 000000000..8d68a177d --- /dev/null +++ b/creativecloud/blocks/nonprofit/constants.js @@ -0,0 +1,427 @@ +// eslint-disable-next-line import/prefer-default-export +export const countries = [ + { + code: 'AFG', + name: 'Afghanistan', + }, + { + code: 'ALA', + name: 'Ă…land Islands', + }, + { + code: 'ALB', + name: 'Albania', + }, + { + code: 'DZA', + name: 'Algeria', + }, + { + code: 'ASM', + name: 'American Samoa', + }, + { + code: 'AND', + name: 'Andorra', + }, + { + code: 'AGO', + name: 'Angola', + }, + { + code: 'AIA', + name: 'Anguilla', + }, + { + code: 'ATA', + name: 'Antarctica', + }, + { + code: 'ARG', + name: 'Argentina', + }, + { + code: 'ABW', + name: 'Aruba', + }, + { + code: 'AUS', + name: 'Australia', + }, + { + code: 'AUT', + name: 'Austria', + }, + { + code: 'BHR', + name: 'Bahrain', + }, + { + code: 'BGD', + name: 'Bangladesh', + }, + { + code: 'BRB', + name: 'Barbados', + }, + { + code: 'BLR', + name: 'Belarus', + }, + { + code: 'BEL', + name: 'Belgium', + }, + { + code: 'BMU', + name: 'Bermuda', + }, + { + code: 'BTN', + name: 'Bhutan', + }, + { + code: 'BIH', + name: 'Bosnia and Herzegovina', + }, + { + code: 'BWA', + name: 'Botswana', + }, + { + code: 'BRA', + name: 'Brazil', + }, + { + code: 'BGR', + name: 'Bulgaria', + }, + { + code: 'CPV', + name: 'Cabo Verde', + }, + { + code: 'KHM', + name: 'Cambodia', + }, + { + code: 'CAN', + name: 'Canada', + }, + { + code: 'CHL', + name: 'Chile', + }, + { + code: 'COL', + name: 'Colombia', + }, + { + code: 'COM', + name: 'Comoros (the)', + }, + { + code: 'COG', + name: 'Congo (the)', + }, + { + code: 'COD', + name: 'Congo (the Democratic Republic of the)', + }, + { + code: 'COK', + name: 'Cook Islands (the)', + }, + { + code: 'HRV', + name: 'Croatia', + }, + { + code: 'CYP', + name: 'Cyprus', + }, + { + code: 'CZE', + name: 'Czechia', + }, + { + code: 'DNK', + name: 'Denmark', + }, + { + code: 'DJI', + name: 'Djibouti', + }, + { + code: 'DMA', + name: 'Dominica', + }, + { + code: 'DOM', + name: 'Dominican Republic (the)', + }, + { + code: 'ECU', + name: 'Ecuador', + }, + { + code: 'EGY', + name: 'Egypt', + }, + { + code: 'EST', + name: 'Estonia', + }, + { + code: 'FIN', + name: 'Finland', + }, + { + code: 'FRA', + name: 'France', + }, + { + code: 'GEO', + name: 'Georgia', + }, + { + code: 'DEU', + name: 'Germany', + }, + { + code: 'GHA', + name: 'Ghana', + }, + { + code: 'GGY', + name: 'Guernsey', + }, + { + code: 'GNB', + name: 'Guinea-Bissau', + }, + { + code: 'HKG', + name: 'Hong Kong', + }, + { + code: 'HUN', + name: 'Hungary', + }, + { + code: 'ISL', + name: 'Iceland', + }, + { + code: 'IND', + name: 'India', + }, + { + code: 'IRL', + name: 'Ireland', + }, + { + code: 'IMN', + name: 'Isle of Man', + }, + { + code: 'ISR', + name: 'Israel', + }, + { + code: 'ITA', + name: 'Italy', + }, + { + code: 'JPN', + name: 'Japan', + }, + { + code: 'JOR', + name: 'Jordan', + }, + { + code: 'XKX', + name: 'Kosovo', + }, + { + code: 'KGZ', + name: 'Kyrgyzstan', + }, + { + code: 'LVA', + name: 'Latvia', + }, + { + code: 'LBN', + name: 'Lebanon', + }, + { + code: 'LUX', + name: 'Luxembourg', + }, + { + code: 'MAC', + name: 'Macao', + }, + { + code: 'MKD', + name: 'Macedonia (the former Yugoslav Republic of)', + }, + { + code: 'MYS', + name: 'Malaysia', + }, + { + code: 'MLT', + name: 'Malta', + }, + { + code: 'MEX', + name: 'Mexico', + }, + { + code: 'FSM', + name: 'Micronesia (Federated States of)', + }, + { + code: 'MDA', + name: 'Moldova (the Republic of)', + }, + { + code: 'MNE', + name: 'Montenegro', + }, + { + code: 'MSR', + name: 'Montserrat', + }, + { + code: 'MAR', + name: 'Morocco', + }, + { + code: 'MOZ', + name: 'Mozambique', + }, + { + code: 'NPL', + name: 'Nepal', + }, + { + code: 'NLD', + name: 'Netherlands (the)', + }, + { + code: 'NZL', + name: 'New Zealand', + }, + { + code: 'NOR', + name: 'Norway', + }, + { + code: 'PSE', + name: 'Palestine, State of', + }, + { + code: 'PAN', + name: 'Panama', + }, + { + code: 'PHL', + name: 'Philippines (the)', + }, + { + code: 'POL', + name: 'Poland', + }, + { + code: 'ROU', + name: 'Romania', + }, + { + code: 'RUS', + name: 'Russian Federation (the)', + }, + { + code: 'SRB', + name: 'Serbia', + }, + { + code: 'SGP', + name: 'Singapore', + }, + { + code: 'SVK', + name: 'Slovakia', + }, + { + code: 'SVN', + name: 'Slovenia', + }, + { + code: 'ZAF', + name: 'South Africa', + }, + { + code: 'ESP', + name: 'Spain', + }, + { + code: 'LKA', + name: 'Sri Lanka', + }, + { + code: 'SWE', + name: 'Sweden', + }, + { + code: 'CHE', + name: 'Switzerland', + }, + { + code: 'TWN', + name: 'Taiwan (Province of China)', + }, + { + code: 'TZA', + name: 'Tanzania, United Republic of', + }, + { + code: 'TUV', + name: 'Tuvalu', + }, + { + code: 'UKR', + name: 'Ukraine', + }, + { + code: 'GBR', + name: 'United Kingdom of Great Britain and Northern Ireland (the)', + }, + { + code: 'USA', + name: 'United States of America (the)', + }, + { + code: 'VUT', + name: 'Vanuatu', + }, + { + code: 'VEN', + name: 'Venezuela (Bolivarian Republic of)', + }, + { + code: 'VNM', + name: 'Viet Nam', + }, + { + code: 'ESH', + name: 'Western Sahara*', + }, + { + code: 'YEM', + name: 'Yemen', + }, +]; diff --git a/creativecloud/blocks/nonprofit/icons.js b/creativecloud/blocks/nonprofit/icons.js new file mode 100644 index 000000000..fe173edc5 --- /dev/null +++ b/creativecloud/blocks/nonprofit/icons.js @@ -0,0 +1,28 @@ +const nonprofitIcons = { + CHEVRON_DOWN: + '', + CHEVRON_RIGHT: + '', + BACK: '', + CLOSE: + '', + UPLOAD: + '', +}; + +export const NONPRFIT_ICONS = Object.freeze({ + CHEVRON_DOWN: 'CHEVRON_DOWN', + CHEVRON_RIGHT: 'CHEVRON_RIGHT', + BACK: 'BACK', + CLOSE: 'CLOSE', + UPLOAD: 'UPLOAD', +}); + +export function getNonprofitIconTag(type) { + const iconString = nonprofitIcons[type]; + const wrapper = document.createElement('div'); + wrapper.innerHTML = iconString; + const icon = wrapper.querySelector('svg'); + icon.classList.add('np-icon'); + return icon; +} diff --git a/creativecloud/blocks/nonprofit/nonprofit-select.js b/creativecloud/blocks/nonprofit/nonprofit-select.js new file mode 100644 index 000000000..49be58110 --- /dev/null +++ b/creativecloud/blocks/nonprofit/nonprofit-select.js @@ -0,0 +1,300 @@ +/* eslint-disable chai-friendly/no-unused-expressions */ +/* eslint-disable max-len */ +import ReactiveStore from './reactiveStore.js'; +import { getNonprofitIconTag, NONPRFIT_ICONS } from './icons.js'; + +export default function nonprofitSelect(props) { + const { + createTag, + name, + label, + placeholder, + noOptionsText = window.mph['nonprofit-no-search-result-found'], + loadingText = window.mph['nonprofit-loading'], + required = true, + disabled = false, + hideIcon = false, + options = [], + store, + debounce, + labelKey = 'label', + valueKey = 'value', + renderOption, + footerTag, + } = props; + + let onInput; + let onSelect; + + const optionsStore = store || new ReactiveStore(options); + let localOptions = [...options]; + let localSelection = null; + + const controlTag = createTag('div', { class: 'np-control' }); + const labelTag = createTag('label', { class: 'np-label', for: name }, label); + const searchTag = createTag('input', { + class: 'np-input np-select-search', + type: 'text', + placeholder, + 'data-for': name, + }); + const valueTag = createTag('input', { + class: `np-select-value${required ? ' np-required-field' : ''}`, + name, + type: 'hidden', + }); + + if (required) { + searchTag.setAttribute('required', 'required'); + valueTag.setAttribute('required', 'required'); + } + if (disabled) { + searchTag.setAttribute('disabled', 'disabled'); + valueTag.setAttribute('disabled', 'disabled'); + } + + const listContainerTag = createTag('div', { class: 'np-select-list-container' }); + const listTag = createTag('ul', { class: 'np-select-list', 'data-for': name }); + + let searchTimeout; + let abortController; + + const showList = () => { + listContainerTag.style.display = 'block'; + }; + + const hideList = () => { + abortController?.abort(); + listTag.scrollTop = 0; + listContainerTag.style.display = 'none'; + + // Handle loss of focus depending on whether there's a selection + if (!localSelection) { + searchTag.value = ''; + if (!store) { + optionsStore.update(localOptions); + } + } else { + searchTag.value = localSelection[labelKey]; + optionsStore.update([localSelection]); + } + }; + + let hasNewInput = false; + + // Search onChange + searchTag.addEventListener('input', () => { + clearTimeout(searchTimeout); + abortController?.abort(); + + hasNewInput = true; + + abortController = new AbortController(); + searchTimeout = setTimeout(() => { + onInput && onInput(searchTag.value, abortController); + if (!store) { + const filteredOptions = localOptions.filter((option) => option[labelKey].toLowerCase().includes(searchTag.value.toLowerCase())); + optionsStore.update(filteredOptions); + } + showList(); + }, debounce); + }); + + let focusedFromList = false; + searchTag.addEventListener('focus', () => { + if (debounce && !searchTag.value) return; + if (!focusedFromList) searchTag.select(); + else focusedFromList = false; + showList(); + }); + + searchTag.addEventListener('keydown', (ev) => { + if (ev.code !== 'ArrowDown') return; + ev.preventDefault(); + if (ev.code === 'ArrowDown') { + const listItem = listContainerTag.querySelector('.np-select-item'); + if (listItem) listItem.focus(); + } + }); + + let rerendering = false; + const focusOut = (ev) => { + if (rerendering) return; + if (!ev.relatedTarget) { + hideList(); + return; + } + const selectTag = ev.relatedTarget.closest('.np-select-list-tag'); + // If the newly focused item is part of the select, don't hide list + if (selectTag || ev.relatedTarget === searchTag) return; + hideList(); + }; + + searchTag.addEventListener('focusout', focusOut); + + let keyboardFocusedId; + + // Render select elements + optionsStore.subscribe((storeOptions, loading) => { + rerendering = true; + + // Empty the list + listTag.replaceChildren(); + + storeOptions.forEach((option) => { + const itemTag = createTag('li', { + class: 'np-select-list-tag np-select-item', + tabindex: -1, + 'data-value': option[valueKey], + }); + + if (renderOption) { + renderOption(option, itemTag); + } else { + itemTag.textContent = option[labelKey]; + } + + // Keyboard navigation and selection handing + + const selectItem = () => { + onSelect && onSelect(option); + searchTag.value = option[labelKey]; + valueTag.value = option[valueKey]; + valueTag.dispatchEvent(new Event('input')); + localSelection = option; + hasNewInput = false; + hideList(); + }; + + itemTag.addEventListener('keydown', (ev) => { + if (!['ArrowDown', 'ArrowUp', 'Enter'].includes(ev.code)) { + focusedFromList = true; + searchTag.focus(); + return; + } + ev.preventDefault(); + + // Select on Enter + if (ev.code === 'Enter') { + selectItem(); + return; + } + + // Navigate on ArrowDown/Up + let sibling; + if (ev.code === 'ArrowDown') { + sibling = ev.target.nextElementSibling; + } + if (ev.code === 'ArrowUp') { + sibling = ev.target.previousElementSibling; + } + if (sibling && !sibling.classList.contains('np-select-loader')) { + sibling.focus(); + } + }); + + itemTag.addEventListener('click', selectItem); + + itemTag.addEventListener('focus', (ev) => { + keyboardFocusedId = ev.target.getAttribute('data-value'); + }); + + itemTag.addEventListener('focusout', (ev) => { + if (rerendering) return; + keyboardFocusedId = null; + focusOut(ev); + }); + + listTag.append(itemTag); + }); + + const infoTagKeydown = () => { + focusedFromList = true; + searchTag.focus(); + }; + + if (!loading && storeOptions.length === 0) { + const noOptionsTag = createTag( + 'div', + { class: 'np-select-list-tag np-select-no-options', tabindex: -1 }, + noOptionsText, + ); + noOptionsTag.addEventListener('keydown', infoTagKeydown); + noOptionsTag.addEventListener('focusout', focusOut); + listTag.append(noOptionsTag); + } + + if (loading) { + const loadingTag = createTag( + 'div', + { class: 'np-select-list-tag np-select-loader', tabindex: -1 }, + `${loadingText}...`, + ); + loadingTag.addEventListener('keydown', infoTagKeydown); + loadingTag.addEventListener('focusout', focusOut); + listTag.append(loadingTag); + } + + if (keyboardFocusedId) { + const itemToFocus = listTag.querySelector(`li[data-value='${keyboardFocusedId}']`); + if (itemToFocus) itemToFocus.focus(); + } + + rerendering = false; + }); + + listContainerTag.append(listTag); + if (footerTag) { + listContainerTag.append(footerTag); + footerTag.addEventListener('focusout', focusOut); + } + + controlTag.append(labelTag, searchTag, valueTag, listContainerTag); + + if (!hideIcon) { + const arrowIconTag = getNonprofitIconTag(NONPRFIT_ICONS.CHEVRON_DOWN); + controlTag.append(arrowIconTag); + } + + controlTag.onInput = (handler) => { + onInput = handler; + }; + + controlTag.onSelect = (handler) => { + onSelect = handler; + }; + + controlTag.onScroll = (handler) => { + listTag.addEventListener('scroll', (ev) => { + abortController = new AbortController(); + handler(ev.target, abortController, hasNewInput); + }); + }; + + controlTag.enable = () => { + searchTag.removeAttribute('disabled'); + valueTag.removeAttribute('disabled'); + }; + + controlTag.clear = (withFocus = true) => { + searchTag.value = ''; + valueTag.value = ''; + valueTag.dispatchEvent(new Event('input')); + localSelection = null; + if (!store) { + optionsStore.update(localOptions); + } + if (withFocus) { + searchTag.focus(); + } + }; + + controlTag.updateOptions = (newOptions) => { + optionsStore.update(newOptions); + localOptions = newOptions; + }; + + controlTag.getValue = () => valueTag.value; + + return controlTag; +} diff --git a/creativecloud/blocks/nonprofit/nonprofit.css b/creativecloud/blocks/nonprofit/nonprofit.css new file mode 100644 index 000000000..ffaca8512 --- /dev/null +++ b/creativecloud/blocks/nonprofit/nonprofit.css @@ -0,0 +1,504 @@ +:root { + --np-content-width: 378px; + --np-container-padding: 86px; + --np-stepper-max-height: 27px; + --np-content-max-height: 727px; + --np-input-placeholder-color: #757575; + --np-organization-item-height: 72px; + --np-organization-cannot-find-height: 51px; + --np-selected-organization-fallback-background: #da408b; + --np-button-disabled-background: #e6e6e6; + --np-button-disabled-color: #b1b1b1; +} + +.np-container { + display: flex; + flex-direction: column; + align-items: center; + padding-block: var(--np-container-padding); + position: relative; + height: calc( + var(--np-stepper-max-height) + var(--np-content-max-height) + 2 * var(--np-container-padding) + ); + box-sizing: border-box; +} + +.np-icon path { + fill: var(--color-gray-600); +} + +.np-stepper-container { + display: flex; + align-items: center; + gap: 12px; + max-height: var(--np-stepper-max-height); +} + +.np-step-container { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.np-step-container span { + user-select: none; +} + +.np-step-icon { + width: 20px; + height: 20px; + border-radius: 10px; + background-color: var(--color-gray-300); + color: var(--color-white); + font-weight: 700; + font-size: 16px; + line-height: 18px; + text-align: center; + vertical-align: middle; +} + +.np-step-name { + font-size: 16px; + color: var(--color-gray-300); +} + +.np-step-container.is-cleared .np-step-icon { + background-color: var(--color-gray-600); +} + +.np-step-container.is-cleared .np-step-name { + color: var(--color-gray-600); +} + +.np-step-container.is-active .np-step-icon { + background-color: var(--link-color); +} + +.np-step-container.is-active .np-step-name { + color: var(--link-color); +} + +.np-step-separator { + width: 12px; + height: 12px; +} + +.np-step-separator.np-icon path { + fill: none; +} + +.np-stepper-back { + position: absolute; + top: 440px; + left: 10%; + width: 50px; + height: 50px; + background-color: var(--color-gray-200); + border-radius: 25px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + transition: left 200ms; +} + +.np-stepper-back:hover, +.np-stepper-back:focus { + background-color: var(--color-gray-300); +} + +.np-stepper-back:hover .np-icon path, +.np-stepper-back:focus .np-icon path { + fill: var(--color-gray-800); +} + +.np-stepper-back.disabled { + background-color: var(--color-gray-100); +} + +.np-content-container { + display: flex; + flex-direction: column; + align-items: center; + max-height: var(--np-content-max-height); +} + +.np-description { + display: flex; + flex-direction: column; + align-items: center; + padding-block: 56px; + gap: 16px; +} + +.np-title { + font-weight: 700; + line-height: 35px; + font-size: 24px; + text-align: center; +} + +.np-subtitle { + text-align: center; + max-width: 800px; +} + +.np-form { + width: var(--np-content-width); + display: flex; + flex-direction: column; + gap: 25px; +} + +.np-button { + background-color: var(--link-color); + color: var(--color-white); + font-weight: 700; + font-size: 16px; + line-height: 20px; + border: none; + border-radius: 20px; + padding: 10px 20px; + cursor: pointer; +} + +.np-button:disabled { + background-color: var(--np-button-disabled-background); + color: var(--np-button-disabled-color); + cursor: default; +} + +.np-form .np-button { + align-self: start; +} + +.np-control { + display: flex; + flex-direction: column; + gap: 7px; + position: relative; +} + +.np-label { + font-weight: 400; + font-size: 14px; + line-height: 18px; + color: var(--color-gray-700); +} + +.np-input { + width: 100%; + border: 1px solid var(--color-gray-500); + line-height: 20px; + padding: 9px 15px; + box-sizing: border-box; + border-radius: 4px; + appearance: none; +} + +.np-input:disabled { + border: 1px solid var(--color-gray-300); +} + +.np-input::placeholder { + color: var(--np-input-placeholder-color); +} + +.np-control .np-icon { + position: absolute; + height: 12px; + right: 15px; + top: 39px; + display: flex; + cursor: pointer; + pointer-events: none; +} + +.np-input:disabled ~ .np-icon path, +.np-stepper-back.disabled .np-icon path { + fill: var(--color-gray-400); +} + +.np-input[type='file'] ~ .np-input { + cursor: pointer; +} + +.np-select-search { + appearance: none; + padding-right: 40px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.np-select-list-container { + display: none; + position: absolute; + list-style-type: none; + border: 1px solid var(--color-gray-500); + background-color: var(--color-white); + border-radius: 4px; + box-sizing: border-box; + width: 100%; + top: 100%; + margin-top: 4px; + min-height: var(--np-organization-cannot-find-height); + max-height: calc( + 4 * var(--np-organization-item-height) + var(--np-organization-cannot-find-height) + 1px + ); + overflow: hidden; + z-index: 1; + box-shadow: 0 2px 6px -1px rgb(0 0 0 / 10%); +} + +.np-select-list { + list-style-type: none; + width: 100%; + margin: 0; + padding: 5px 0; + max-height: calc(4 * var(--np-organization-item-height)); + overflow-x: hidden; + overflow-y: auto; +} + +.np-select-list-tag { + padding: 10px 15px; + font-size: 16px; + line-height: 21px; + cursor: pointer; + display: flex; + flex-direction: column; + outline: none; + overflow: hidden; +} + +.np-select-item:hover { + background-color: var(--color-gray-100); +} + +.np-select-item:focus { + background-color: var(--color-gray-200); +} + +.np-select-item span { + line-height: 21px; + width: 100%; +} + +.np-organization-select-name { + font-weight: 700; + padding-bottom: 10px; +} + +.np-organization-select-id { + font-weight: 400; +} + +.np-organization-select-name, +.np-organization-select-id { + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.np-select-no-options { + color: var(--np-input-placeholder-color); +} + +.np-select-no-options, +.np-select-loader { + user-select: none; +} + +.np-selected-organization-container { + display: none; + border: 1px solid var(--color-gray-500); + border-radius: 4px; + box-sizing: border-box; + flex-direction: column; + padding-inline: 15px; + position: relative; +} + +.np-selected-organization-container .np-selected-organization-detail { + font-size: 16px; + font-weight: 400; + line-height: 21px; + padding-block: 10px; +} + +.np-selected-organization-container > .np-selected-organization-detail { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.np-selected-organization-header { + display: flex; + align-items: center; + padding-block: 10px; + gap: 10px; +} + +.np-selected-organization-header .np-selected-organization-detail { + padding-right: 15px; +} + +.np-selected-organization-avatar { + width: 40px; + height: 40px; + border-radius: 20px; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.np-selected-organization-avatar.loading { + animation: loadingAvatar 1s infinite; +} + +.np-selected-organization-avatar.fallback { + background-color: var(--np-selected-organization-fallback-background); +} + +.np-selected-organization-logo { + object-fit: contain; +} + +.np-selected-organization-avatar.fallback .np-selected-organization-logo { + display: none; +} + +.np-selected-organization-initials { + color: var(--color-white); + font-size: 18px; + font-weight: 700; +} + +.np-selected-organization-avatar:not(.fallback) .np-selected-organization-initials { + display: none; +} + +.np-selected-organization-separator { + height: 1px; + background-color: var(--color-gray-300); +} + +.np-selected-organization-clear { + position: absolute; + display: flex; + top: 15px; + right: 15px; + cursor: pointer; +} + +.np-selected-organization-clear:hover .np-icon path, +.np-selected-organization-clear:focus .np-icon path { + fill: var(--color-gray-800); +} + +.np-personal-data-disclaimer { + font-size: 14px; + line-height: 18px; + padding-bottom: 15px; + color: var(--color-gray-600); +} + +.np-application-review-container { + display: flex; + flex-direction: column; + align-items: center; + margin-block: 56px; +} + +.np-application-review-container .np-title { + margin-bottom: 16px; +} + +.np-application-review-detail { + margin-block: 16px; + font-size: 18px; + line-height: 24px; + font-weight: 400; +} + +@media screen and (max-width: 900px) { + .np-stepper-back { + left: 4%; + } +} + +@keyframes loadingAvatar { + 0% { + background-color: var(--color-gray-200); + } + + 50% { + background-color: var(--color-gray-100); + } + + 100% { + background-color: var(--color-gray-200); + } +} + +/* Temporary stuff below (TODO - remove) */ +.nonprofit { + display: flex; + flex-direction: column; +} + +.np-controller-buffer { + flex-grow: 1; +} + +.np-controller-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 50px; + gap: 10px; +} + +.np-controller-title { + font-weight: 700; + font-size: 16px; + color: var(--link-color); +} + +.np-controller-button { + border: 2px solid var(--link-color); + background-color: var(--color-white); + color: var(--link-color); + font-weight: 700; + font-size: 16px; + line-height: 20px; + border-radius: 20px; + padding: 10px 20px; + align-self: start; + cursor: pointer; + width: 200px; + margin-right: 12px; + white-space: nowrap; + + &.is-step { + width: 100px; + margin-right: 3px; + } + + &.selected { + background-color: var(--link-color); + color: var(--color-white); + } +} + +.np-controller-section { + display: flex; + width: 415px; + + & .np-controller-button:last-child { + margin: 0; + } +} diff --git a/creativecloud/blocks/nonprofit/nonprofit.js b/creativecloud/blocks/nonprofit/nonprofit.js new file mode 100644 index 000000000..61f172ef5 --- /dev/null +++ b/creativecloud/blocks/nonprofit/nonprofit.js @@ -0,0 +1,970 @@ +/* eslint-disable no-alert */ +/* eslint-disable no-plusplus */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable max-len */ +import ReactiveStore from './reactiveStore.js'; +import { setLibs } from '../../scripts/utils.js'; +import { countries } from './constants.js'; +import { getNonprofitIconTag, NONPRFIT_ICONS } from './icons.js'; +import nonprofitSelect from './nonprofit-select.js'; + +const miloLibs = setLibs('/libs'); +const { createTag } = await import(`${miloLibs}/utils/utils.js`); + +const removeOptionElements = (element) => { + const children = element.querySelectorAll(':scope > div'); + children.forEach((child) => { + child.remove(); + }); +}; + +// #region Constants + +const PERCENT_API_URL = 'https://sandbox-api.poweredbypercent.com/v1'; +const PERCENT_VALIDATION_API_URL = 'https://sandbox-validate.poweredbypercent.com/adobe-acrobat'; +const PERCENT_PUBLISHABLE_KEY = 'sandbox_pk_8b320cc4-5950-4263-a3ac-828c64f6e19b'; +export const SCENARIOS = Object.freeze({ + FOUND_IN_SEARCH: 'FOUND_IN_SEARCH', + NOT_FOUND_IN_SEARCH: 'NOT_FOUND_IN_SEARCH', +}); +const SEARCH_DEBOUNCE = 500; // ms +const FETCH_ON_SCROLL_OFFSET = 100; // px +// #endregion + +const nonprofitFormData = JSON.parse('{}'); + +// #region Stores +export const stepperStore = new ReactiveStore({ + step: 1, + scenario: SCENARIOS.FOUND_IN_SEARCH, + pending: false, +}); + +export const organizationsStore = new ReactiveStore([]); + +export const registriesStore = new ReactiveStore([]); + +const selectedOrganizationStore = new ReactiveStore(); +// #endregion + +// #region Percent API integration + +// #region Helpers + +function getPercentErrorString(result) { + return `${result.error.title}: ${result.error.message}${result.error.reasons ? ` (${result.error.reasons.join(', ')})` : ''}`; +} + +async function validatePercentResponse(response) { + const result = await response.json(); + + if (!response.ok) { + throw new Error(getPercentErrorString(result)); + } + + return result; +} + +// #endregion + +let nextOrganizationsPageUrl; + +async function fetchOrganizations(search, countryCode, abortController) { + try { + organizationsStore.startLoading(true); + const response = await fetch( + `${PERCENT_API_URL}/organisations?countryCode=${countryCode}&query=${search}`, + { + cache: 'force-cache', + signal: abortController.signal, + headers: { Authorization: PERCENT_PUBLISHABLE_KEY }, + }, + ); + + const result = await validatePercentResponse(response); + + if (!result._links) { + nextOrganizationsPageUrl = null; + window.lana?.log('No next organization page link provided.'); + } else nextOrganizationsPageUrl = result._links.next || null; + organizationsStore.update(result.data); + } catch (error) { + organizationsStore.update((prev) => prev); + window.lana?.log(`Could not fetch organizations: ${error}`); + } +} + +async function fetchNextOrganizations(abortController) { + if (!nextOrganizationsPageUrl) return; + try { + organizationsStore.startLoading(); + const response = await fetch(nextOrganizationsPageUrl, { + cache: 'force-cache', + signal: abortController.signal, + headers: { Authorization: PERCENT_PUBLISHABLE_KEY }, + }); + + const result = await validatePercentResponse(response); + + nextOrganizationsPageUrl = result._links.next; + organizationsStore.update((prev) => [...prev, ...result.data]); + } catch (error) { + organizationsStore.update((prev) => prev); + window.lana?.log(`Could not fetch next organizations: ${error}`); + } +} + +async function fetchRegistries(countryCode, abortController) { + try { + registriesStore.startLoading(true); + const response = await fetch(`${PERCENT_API_URL}/registries?countryCode=${countryCode}`, { + cache: 'force-cache', + signal: abortController.signal, + headers: { Authorization: PERCENT_PUBLISHABLE_KEY }, + }); + + const result = await validatePercentResponse(response); + + registriesStore.update(result.data); + } catch (error) { + registriesStore.update((prev) => prev); + window.lana?.log(`Could not fetch registries: ${error}`); + } +} + +async function sendOrganizationData() { + try { + const inviteResponse = await fetch(PERCENT_VALIDATION_API_URL, { + method: 'POST', + headers: { Authorization: `Bearer ${PERCENT_PUBLISHABLE_KEY}` }, + }); + + const inviteResult = await validatePercentResponse(inviteResponse); + + const { validationInviteId } = inviteResult.data; + + const foundInSearch = stepperStore.data.scenario === SCENARIOS.FOUND_IN_SEARCH; + + if (!foundInSearch) { + const evidenceUploadData = new FormData(); + evidenceUploadData.append('file', nonprofitFormData.evidenceNonProfitStatus); + evidenceUploadData.append('validationInviteId', validationInviteId); + + const uploadResponse = await fetch(`${PERCENT_API_URL}/validation-submission-documents`, { + method: 'POST', + headers: { Authorization: PERCENT_PUBLISHABLE_KEY }, + body: evidenceUploadData, + }); + + await validatePercentResponse(uploadResponse); + } + + let body; + if (foundInSearch) { + body = JSON.stringify({ + validationInviteId, + organisationId: nonprofitFormData.organizationId, + firstName: nonprofitFormData.firstName, + lastName: nonprofitFormData.lastName, + email: nonprofitFormData.email, + language: 'en-US', + }); + } else { + body = JSON.stringify({ + validationInviteId, + countryCode: nonprofitFormData.countryCode, + organisationName: nonprofitFormData.organizationName, + registryId: nonprofitFormData.organizationRegistrationId, + registryName: nonprofitFormData.registryName, + website: nonprofitFormData.website, + addressLine1: nonprofitFormData.streetAddress, + addressLine2: nonprofitFormData.addressDetails, + city: nonprofitFormData.city, + postal: nonprofitFormData.zipCode, + state: nonprofitFormData.state, + firstName: nonprofitFormData.firstName, + lastName: nonprofitFormData.lastName, + email: nonprofitFormData.email, + language: 'en-US', + }); + } + + const submissionResponse = await fetch(`${PERCENT_API_URL}/validation-submissions`, { + method: 'POST', + body, + headers: { + Authorization: PERCENT_PUBLISHABLE_KEY, + 'Content-Type': 'application/json; charset=utf-8', + }, + }); + + await validatePercentResponse(submissionResponse); + + return true; + } catch (error) { + window.lana?.log(`Could not send organization data: ${error}`); + return false; + } +} + +// #endregion + +// UI + +function getStepBackTag() { + const buttonTag = createTag('div', { class: 'np-stepper-back', tabindex: 0 }); + const backIconTag = getNonprofitIconTag(NONPRFIT_ICONS.BACK); + buttonTag.append(backIconTag); + + stepperStore.subscribe(({ step, scenario, pending }) => { + if (pending) buttonTag.classList.add('disabled'); + else buttonTag.classList.remove('disabled'); + if (step === 1 || (step === 3 && scenario === SCENARIOS.FOUND_IN_SEARCH) || step === 5) { + buttonTag.style.display = 'none'; + return; + } + buttonTag.style.display = 'flex'; + }); + + buttonTag.addEventListener('click', () => { + if (stepperStore.data.pending) return; + stepperStore.update((prev) => ({ ...prev, step: prev.step - 1 })); + }); + + buttonTag.addEventListener('keydown', (ev) => { + if (stepperStore.data.pending) return; + if (ev.code !== 'Enter') return; + ev.preventDefault(); + buttonTag.click(); + }); + + return buttonTag; +} + +function renderStepper(containerTag) { + const stepperContainerTag = createTag('div', { class: 'np-stepper-container' }); + const getStepTag = (number) => { + const stepContainerTag = createTag('div', { class: 'np-step-container', 'data-step': number }); + const stepIconTag = createTag('span', { class: 'np-step-icon' }, number); + const stepNameTag = createTag( + 'span', + { class: 'np-step-name' }, + window.mph[`nonprofit-step-${number}`], + ); + stepContainerTag.append(stepIconTag, stepNameTag); + return stepContainerTag; + }; + + const step1 = getStepTag(1); + const step2 = getStepTag(2); + const step3 = getStepTag(3); + + stepperStore.subscribe(({ step, scenario }) => { + // Reset steps + step1.classList.remove('is-cleared', 'is-active'); + step2.classList.remove('is-cleared', 'is-active'); + step3.classList.remove('is-cleared', 'is-active'); + + if (step === 1) { + step1.classList.add('is-active'); + } + if (step === 2) { + step1.classList.add('is-cleared'); + step2.classList.add('is-active'); + } + if (step === 3) { + if (scenario === SCENARIOS.FOUND_IN_SEARCH) { + step1.classList.add('is-cleared'); + step2.classList.add('is-cleared'); + step3.classList.add('is-active'); + } else { + step1.classList.add('is-cleared'); + step2.classList.add('is-active'); + } + } + if (step === 4) { + step1.classList.add('is-cleared'); + step2.classList.add('is-active'); + } + if (step === 5) { + step1.classList.add('is-cleared'); + step2.classList.add('is-cleared'); + step3.classList.add('is-active'); + } + }); + + const separatorIconTag = getNonprofitIconTag(NONPRFIT_ICONS.CHEVRON_RIGHT); + separatorIconTag.classList.add('np-step-separator'); + + stepperContainerTag.append( + step1, + separatorIconTag.cloneNode(true), + step2, + separatorIconTag.cloneNode(true), + step3, + ); + + const stepBackTag = getStepBackTag(); + + containerTag.append(stepperContainerTag, stepBackTag); +} + +// #region Render form + +function getDescriptionTag(title, subtitle) { + const descriptionTag = createTag('div', { class: 'np-description' }); + const titleTag = createTag('span', { class: 'np-title' }, title); + + descriptionTag.append(titleTag); + + if (subtitle) { + const subtitleTag = createTag('span', { class: 'np-subtitle' }, subtitle); + + descriptionTag.append(subtitleTag); + } + + return descriptionTag; +} + +function getSubmitTag() { + return createTag('input', { + class: 'np-button', + type: 'submit', + value: window.mph['nonprofit-continue'], + disabled: 'disabled', + }); +} + +function getNonprofitInput(params) { + const { type, name, label, placeholder, required } = params; + const baseParams = { name, placeholder }; + if (required) baseParams.required = 'required'; + const controlTag = createTag('div', { class: 'np-control' }); + const labelTag = createTag('label', { class: 'np-label', for: name }, label); + const inputTag = createTag('input', { + class: `np-input${required ? ' np-required-field' : ''}`, + type, + ...baseParams, + }); + controlTag.append(labelTag, inputTag); + + // File validation + if (type === 'file') { + // Hide input and render a text one + inputTag.style.display = 'none'; + const textTag = createTag('input', { + type: 'text', + class: 'np-input', + placeholder, + readonly: 'readonly', + 'data-for': name, + }); + + textTag.addEventListener('click', () => { + inputTag.click(); + }); + + textTag.addEventListener('keypress', (ev) => { + if (ev.code !== 'Enter') return; + ev.preventDefault(); + inputTag.click(); + }); + + // Validation + inputTag.addEventListener('change', () => { + if (!inputTag.files || inputTag.files.length === 0) { + textTag.value = ''; + return; + } + + const file = inputTag.files[0]; + + // Percent only accepts jpg, png and pdf files + const extensionRegex = /(\.jpg|\.jpeg|\.png|\.pdf)$/i; + if (!extensionRegex.exec(file.name)) { + inputTag.value = ''; + inputTag.dispatchEvent(new Event('input')); + alert(window.mph['nonprofit-invalid-file-type']); + return; + } + + // Percent acceps files up to 5 mb + const size = file.size / 1024 ** 2; + if (size > 5) { + inputTag.value = ''; + inputTag.dispatchEvent(new Event('input')); + alert(window.mph['nonprofit-file-size-exceeded']); + return; + } + + textTag.value = file.name; + }); + + const uploadIconTag = getNonprofitIconTag(NONPRFIT_ICONS.UPLOAD); + + controlTag.append(textTag, uploadIconTag); + } + + return controlTag; +} + +function getSelectedOrganizationTag() { + const containerTag = createTag('div', { class: 'np-selected-organization-container' }); + + const headerTag = createTag('div', { class: 'np-selected-organization-header' }); + + const avatarTag = createTag('div', { class: 'np-selected-organization-avatar' }); + + const initialsTag = createTag('span', { class: 'np-selected-organization-initials' }); + const showInitials = () => { + avatarTag.classList.add('fallback'); + const initialWords = selectedOrganizationStore.data.name + .split(' ') + .filter((word) => Boolean(word)) + .slice(0, 2); + const initials = initialWords.map((word) => word.substring(0, 1).toUpperCase()).join(''); + initialsTag.textContent = initials; + }; + + const logoTag = createTag('img', { class: 'np-selected-organization-logo' }); + logoTag.addEventListener('error', () => { + avatarTag.classList.remove('loading'); + showInitials(); + }); + logoTag.addEventListener('load', () => { + avatarTag.classList.remove('loading'); + }); + + avatarTag.append(initialsTag, logoTag); + + const nameTag = createTag('span', { class: 'np-selected-organization-detail' }); + headerTag.append(avatarTag, nameTag); + + const separatorTag = createTag('div', { class: 'np-selected-organization-separator' }); + + const addressTag = createTag('span', { class: 'np-selected-organization-detail' }); + const idTag = createTag('span', { class: 'np-selected-organization-detail' }); + + const clearTag = createTag('div', { class: 'np-selected-organization-clear', tabindex: 0 }); + const clearIconTag = getNonprofitIconTag(NONPRFIT_ICONS.CLOSE); + clearTag.append(clearIconTag); + + clearTag.addEventListener('keydown', (ev) => { + if (ev.code !== 'Enter') return; + clearTag.click(); + }); + + containerTag.append(headerTag, separatorTag, addressTag, idTag, clearTag); + + selectedOrganizationStore.subscribe((organization) => { + if (!organization) { + containerTag.style.display = 'none'; + return; + } + + // Load avatar + if (organization.logo) { + avatarTag.classList.add('loading'); + avatarTag.classList.remove('fallback'); + logoTag.src = organization.logo; + } else { + showInitials(); + } + + nameTag.textContent = organization.name; + + addressTag.textContent = organization.address || '-'; + addressTag.setAttribute('title', organization.address); + idTag.textContent = organization.id; + idTag.setAttribute('title', organization.id); + + containerTag.style.display = 'flex'; + }, false); + + containerTag.onClear = (handler) => { + clearTag.addEventListener('click', handler); + }; + + return containerTag; +} + +function trackSubmitCondition(formTag) { + const requiredInputs = formTag.querySelectorAll('.np-required-field'); + const submitTag = formTag.querySelector('.np-button[type=submit]'); + + for (let index = 0; index < requiredInputs.length; index++) { + const requiredInput = requiredInputs[index]; + requiredInput.addEventListener('input', () => { + if (!requiredInput.value) { + submitTag.setAttribute('disabled', 'disabled'); + } else { + let hasEmptyFields = false; + requiredInputs.forEach((input) => { + if (input === requiredInput) return; + if (!input.value) hasEmptyFields = true; + }); + if (hasEmptyFields) { + submitTag.setAttribute('disabled', 'disabled'); + } else { + submitTag.removeAttribute('disabled'); + } + } + }); + } +} + +// Select non-profit +function renderSelectNonprofit(containerTag) { + // Description + const descriptionTag = getDescriptionTag( + window.mph['nonprofit-title-select-non-profit'], + window.mph['nonprofit-subtitle-select-non-profit'], + ); + + // Form + const formTag = createTag('form', { class: 'np-form' }); + + const countryTag = nonprofitSelect({ + createTag, + name: 'country', + label: window.mph['nonprofit-country'], + placeholder: window.mph['nonprofit-country-placeholder'], + options: countries, + labelKey: 'name', + valueKey: 'code', + }); + + // #region Organization select + const organizationTag = nonprofitSelect({ + createTag, + name: 'organizationId', + label: window.mph['nonprofit-organization-name-or-id'], + placeholder: window.mph['nonprofit-organization-name-or-id-search-placeholder'], + noOptionsText: window.mph['nonprofit-not-found-in-database'], + debounce: SEARCH_DEBOUNCE, + store: organizationsStore, + disabled: true, + hideIcon: true, + clearable: true, + labelKey: 'name', + valueKey: 'id', + renderOption: (option, itemTag) => { + const nameTag = createTag( + 'span', + { class: 'np-organization-select-name', title: option.name }, + option.name, + ); + const idTag = createTag( + 'span', + { class: 'np-organization-select-id', title: option.id }, + option.id, + ); + + itemTag.append(nameTag, idTag); + }, + footerTag: (() => { + const cannotFindTag = createTag('div', { class: 'np-select-list-tag np-organization-cannot-find' }); + const cannotFindLinkTag = createTag( + 'a', + { tabindex: 0 }, + window.mph['nonprofit-organization-cannot-find'], + ); + // Cannot find action handler + const switchToNotFound = () => { + stepperStore.update((prev) => ({ + ...prev, + step: 2, + scenario: SCENARIOS.NOT_FOUND_IN_SEARCH, + })); + }; + cannotFindLinkTag.addEventListener('click', switchToNotFound); + cannotFindLinkTag.addEventListener('keydown', (ev) => { + if (ev.code !== 'Enter') return; + switchToNotFound(); + }); + + cannotFindTag.append(cannotFindLinkTag); + + return cannotFindTag; + })(), + }); + + organizationTag.onInput((value, abortController) => { + if (!value) return; + fetchOrganizations(value, countryTag.getValue(), abortController); + }); + + organizationTag.onSelect((option) => { + selectedOrganizationStore.update(option); + }); + + organizationTag.onScroll((listTag, abortController, hasNewInput) => { + if ( + (Boolean(selectedOrganizationStore.data) && !hasNewInput) + || organizationsStore.loading + || !nextOrganizationsPageUrl + ) return; + if (listTag.scrollTop + listTag.clientHeight + FETCH_ON_SCROLL_OFFSET >= listTag.scrollHeight) { + fetchNextOrganizations(abortController); + } + }); + + countryTag.onSelect(() => { + organizationTag.enable(); + organizationTag.clear(); + if (selectedOrganizationStore.data) { + selectedOrganizationStore.update(null); + } + }); + + // #endregion + + const selectedOrganizationTag = getSelectedOrganizationTag(); + + selectedOrganizationTag.onClear(() => { + organizationTag.clear(); + selectedOrganizationStore.update(null); + }); + + const submitTag = getSubmitTag(); + + formTag.append(countryTag, organizationTag, selectedOrganizationTag, submitTag); + + trackSubmitCondition(formTag); + + formTag.addEventListener('submit', (ev) => { + ev.preventDefault(); + + const formData = new FormData(formTag); + nonprofitFormData.countryCode = formData.get('country'); + nonprofitFormData.organizationId = formData.get('organizationId'); + + stepperStore.update((prev) => ({ ...prev, scenario: SCENARIOS.FOUND_IN_SEARCH, step: 2 })); + }); + + containerTag.replaceChildren(descriptionTag, formTag); +} + +// Organization details +function renderOrganizationDetails(containerTag) { + // Description + const descriptionTag = getDescriptionTag(window.mph['nonprofit-title-organization-details']); + + // Form + const formTag = createTag('form', { class: 'np-form' }); + + let abortController; + + const countryTag = nonprofitSelect({ + createTag, + name: 'country', + label: window.mph['nonprofit-country'], + placeholder: window.mph['nonprofit-country-placeholder'], + options: countries, + labelKey: 'name', + valueKey: 'code', + }); + + countryTag.onSelect((option) => { + abortController?.abort(); + abortController = new AbortController(); + fetchRegistries(option.code, abortController); + }); + + const organizationNameTag = getNonprofitInput({ + type: 'text', + name: 'organizationName', + label: window.mph['nonprofit-organization-name'], + placeholder: window.mph['nonprofit-organization-name-placeholder'], + required: true, + }); + + const registryTag = nonprofitSelect({ + createTag, + name: 'registry', + label: window.mph['nonprofit-registry'], + placeholder: window.mph['nonprofit-registry-placeholder'], + labelKey: 'name', + valueKey: 'name', + disabled: true, + }); + + registriesStore.subscribe((registries, loading) => { + if (!countryTag.getValue()) return; + if (loading) { + registryTag.clear(false); + return; + } + registryTag.enable(); + registryTag.updateOptions(registries); + }); + + const organizationRegistrationIdTag = getNonprofitInput({ + type: 'text', + name: 'organizationRegistrationId', + label: window.mph['nonprofit-organization-registration-id'], + placeholder: window.mph['nonprofit-organization-registration-id-placeholder'], + required: true, + }); + + const evidenceNonProfitStatusTag = getNonprofitInput({ + type: 'file', + name: 'evidenceNonProfitStatus', + label: window.mph['nonprofit-evidence-non-profit-status'], + placeholder: window.mph['nonprofit-evidence-non-profit-status-placeholder'], + required: true, + }); + + const websiteTag = getNonprofitInput({ + type: 'text', + name: 'website', + label: window.mph['nonprofit-website'], + placeholder: window.mph['nonprofit-website-placeholder'], + required: true, + }); + + const submitTag = getSubmitTag(); + + formTag.append( + countryTag, + organizationNameTag, + registryTag, + organizationRegistrationIdTag, + evidenceNonProfitStatusTag, + websiteTag, + submitTag, + ); + + trackSubmitCondition(formTag); + + formTag.addEventListener('submit', (ev) => { + ev.preventDefault(); + + const formData = new FormData(formTag); + nonprofitFormData.countryCode = formData.get('country'); + nonprofitFormData.organizationName = formData.get('organizationName'); + nonprofitFormData.registryName = formData.get('registry'); + nonprofitFormData.organizationRegistrationId = formData.get('organizationRegistrationId'); + nonprofitFormData.evidenceNonProfitStatus = formData.get('evidenceNonProfitStatus'); + nonprofitFormData.website = formData.get('website'); + + stepperStore.update((prev) => ({ ...prev, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH, step: 3 })); + }); + + containerTag.replaceChildren(descriptionTag, formTag); +} + +// Organization address +function renderOrganizationAddress(containerTag) { + // Description + const descriptionTag = getDescriptionTag(window.mph['nonprofit-title-organization-address']); + + // Form + const formTag = createTag('form', { class: 'np-form' }); + + const streetAddressTag = getNonprofitInput({ + type: 'text', + name: 'streetAddress', + label: window.mph['nonprofit-street-address'], + placeholder: window.mph['nonprofit-street-address-placeholder'], + required: true, + }); + + const addressDetailsTag = getNonprofitInput({ + type: 'text', + name: 'addressDetails', + label: window.mph['nonprofit-address-details'], + placeholder: window.mph['nonprofit-address-details-placeholder'], + }); + + const stateTag = getNonprofitInput({ + type: 'text', + name: 'state', + label: window.mph['nonprofit-state'], + placeholder: window.mph['nonprofit-state-placeholder'], + }); + + const cityTag = getNonprofitInput({ + type: 'text', + name: 'city', + label: window.mph['nonprofit-city'], + placeholder: window.mph['nonprofit-city-placeholder'], + required: true, + }); + + const zipCodeTag = getNonprofitInput({ + type: 'text', + name: 'zipCode', + label: window.mph['nonprofit-zip-code'], + placeholder: window.mph['nonprofit-zip-code-placeholder'], + required: true, + }); + + const submitTag = getSubmitTag(); + + formTag.append(streetAddressTag, addressDetailsTag, stateTag, cityTag, zipCodeTag, submitTag); + + trackSubmitCondition(formTag); + + formTag.addEventListener('submit', (ev) => { + ev.preventDefault(); + + const formData = new FormData(formTag); + nonprofitFormData.streetAddress = formData.get('streetAddress'); + nonprofitFormData.addressDetails = formData.get('addressDetails'); + nonprofitFormData.state = formData.get('state'); + nonprofitFormData.city = formData.get('city'); + nonprofitFormData.zipCode = formData.get('zipCode'); + + stepperStore.update((prev) => ({ ...prev, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH, step: 4 })); + }); + + containerTag.replaceChildren(descriptionTag, formTag); +} + +// Personal data +function renderPersonalData(containerTag) { + // Description + const descriptionTag = getDescriptionTag( + window.mph['nonprofit-title-personal-details'], + window.mph['nonprofit-subtitle-personal-details'], + ); + + // Form + const formTag = createTag('form', { class: 'np-form' }); + + const firstNameTag = getNonprofitInput({ + type: 'text', + name: 'firstName', + label: window.mph['nonprofit-first-name'], + placeholder: window.mph['nonprofit-first-name-placeholder'], + required: true, + }); + + const lastNameTag = getNonprofitInput({ + type: 'text', + name: 'lastName', + label: window.mph['nonprofit-last-name'], + placeholder: window.mph['nonprofit-last-name-placeholder'], + required: true, + }); + + const emailTag = getNonprofitInput({ + type: 'text', + name: 'email', + label: window.mph['nonprofit-email'], + placeholder: window.mph['nonprofit-email-placeholder'], + required: true, + }); + + const disclaimerTag = createTag( + 'span', + { class: 'np-personal-data-disclaimer' }, + window.mph['nonprofit-personal-data-disclaimer'], + ); + + const submitTag = getSubmitTag(); + + formTag.append(firstNameTag, lastNameTag, emailTag, disclaimerTag, submitTag); + + trackSubmitCondition(formTag); + + formTag.addEventListener('submit', async (ev) => { + ev.preventDefault(); + + const formData = new FormData(formTag); + nonprofitFormData.firstName = formData.get('firstName'); + nonprofitFormData.lastName = formData.get('lastName'); + nonprofitFormData.email = formData.get('email'); + + const inputs = formTag.querySelectorAll('input'); + inputs.forEach((input) => { + input.setAttribute('disabled', 'disabled'); + }); + + stepperStore.update((prev) => ({ ...prev, pending: true })); + + const ok = await sendOrganizationData(); + + if (!ok) { + inputs.forEach((input) => { + input.removeAttribute('disabled'); + }); + + stepperStore.update((prev) => ({ ...prev, pending: false })); + } else { + stepperStore.update((prev) => ({ ...prev, step: prev.step + 1 })); + } + }); + + containerTag.replaceChildren(descriptionTag, formTag); +} + +function renderApplicationReview(containerTag) { + const applicationReviewTag = createTag('div', { class: 'np-application-review-container' }); + + const titleTag = createTag( + 'span', + { class: 'np-title' }, + window.mph['nonprofit-title-application-review'], + ); + const detail1Tag = createTag( + 'span', + { class: 'np-application-review-detail' }, + window.mph['nonprofit-detail-1-application-review'], + ); + const detail2Tag = createTag( + 'span', + { class: 'np-application-review-detail' }, + window.mph['nonprofit-detail-2-application-review']?.replace( + '__EMAIL__', + nonprofitFormData.email, + ), + ); + + applicationReviewTag.append(titleTag, detail1Tag, detail2Tag); + + const returnToAcrobatForNonprofitsTag = createTag( + 'button', + { class: 'np-button' }, + window.mph['nonprofit-return-to-acrobat-for-nonprofits'], + ); + + containerTag.replaceChildren(applicationReviewTag, returnToAcrobatForNonprofitsTag); +} + +function renderStepContent(containerTag) { + const contentContainerTag = createTag('div', { class: 'np-content-container' }); + + let currentStep; + let currentScenario; + stepperStore.subscribe(({ step, scenario }) => { + if (step === currentStep && scenario === currentScenario) return; + currentStep = step; + currentScenario = scenario; + + if (step === 1) renderSelectNonprofit(contentContainerTag); + if (step === 2 && scenario === SCENARIOS.FOUND_IN_SEARCH) renderPersonalData(contentContainerTag); + if (step === 2 && scenario === SCENARIOS.NOT_FOUND_IN_SEARCH) renderOrganizationDetails(contentContainerTag); + if (step === 3 && scenario === SCENARIOS.FOUND_IN_SEARCH) renderApplicationReview(contentContainerTag); + if (step === 3 && scenario === SCENARIOS.NOT_FOUND_IN_SEARCH) renderOrganizationAddress(contentContainerTag); + if (step === 4 && scenario === SCENARIOS.NOT_FOUND_IN_SEARCH) renderPersonalData(contentContainerTag); + if (step === 5 && scenario === SCENARIOS.NOT_FOUND_IN_SEARCH) renderApplicationReview(contentContainerTag); + }); + + containerTag.append(contentContainerTag); +} +// #endregion + +function initNonprofit(element) { + const containerTag = createTag('div', { class: 'np-container' }); + renderStepper(containerTag); + renderStepContent(containerTag); + element.append(containerTag); +} + +export default function init(element) { + // Get metadata + removeOptionElements(element); + initNonprofit(element); +} diff --git a/creativecloud/blocks/nonprofit/reactiveStore.js b/creativecloud/blocks/nonprofit/reactiveStore.js new file mode 100644 index 000000000..856dd704d --- /dev/null +++ b/creativecloud/blocks/nonprofit/reactiveStore.js @@ -0,0 +1,50 @@ +export default class ReactiveStore { + data = null; + + loaded = false; + + loading = false; + + #initialData = null; + + #subscribers = []; + + constructor(initialData = null) { + if (initialData) this.#initialData = initialData; + this.data = initialData; + } + + subscribe(fn, withTrigger = true) { + if (!fn) return; + if (!this.#subscribers.includes(fn)) this.#subscribers.push(fn); + if (!withTrigger) return; + fn(this.data, this.loading); + } + + unsubscribe(fn) { + const indexOfFn = this.#subscribers.indexOf(fn); + if (indexOfFn !== -1) this.#subscribers.splice(indexOfFn, 1); + } + + unsubscribeAll() { + this.#subscribers = []; + } + + update(data) { + if (typeof data === 'function') this.data = data(this.data); + else this.data = data; + this.loaded = true; + this.loading = false; + this.#subscribers.forEach((subscriber) => { + subscriber(this.data, this.loading); + }); + } + + startLoading(resetData = false) { + this.loading = true; + if (resetData) this.data = this.#initialData; + this.#subscribers.forEach((subscriber) => { + subscriber(this.data, this.loading); + }); + } +} diff --git a/creativecloud/blocks/sidenav/sidenav.js b/creativecloud/blocks/sidenav/sidenav.js index 368ba273c..d6d8fcdf7 100644 --- a/creativecloud/blocks/sidenav/sidenav.js +++ b/creativecloud/blocks/sidenav/sidenav.js @@ -163,7 +163,7 @@ export default async function init(el) { const libs = getLibs(); const [mainRow, categoryRow] = Array.from(el.children); const deps = Promise.all([ - import('../../deps/merch-sidenav.js'), + import(`${libs}/deps/mas/merch-sidenav.js`), // eslint-disable-next-line import/no-unresolved, import/no-absolute-path import(`${libs}/deps/lit-all.min.js`), import(`${libs}/features/spectrum-web-components/dist/theme.js`), diff --git a/creativecloud/deps/merch-sidenav.js b/creativecloud/deps/merch-sidenav.js deleted file mode 100644 index 09057e8ec..000000000 --- a/creativecloud/deps/merch-sidenav.js +++ /dev/null @@ -1,141 +0,0 @@ -import{html as k,css as H,LitElement as P}from"/libs/deps/lit-all.min.js";var r=class{constructor(e,t){this.key=Symbol("match-media-key"),this.matches=!1,this.host=e,this.host.addController(this),this.media=window.matchMedia(t),this.matches=this.media.matches,this.onChange=this.onChange.bind(this),e.addController(this)}hostConnected(){var e;(e=this.media)==null||e.addEventListener("change",this.onChange)}hostDisconnected(){var e;(e=this.media)==null||e.removeEventListener("change",this.onChange)}onChange(e){this.matches!==e.matches&&(this.matches=e.matches,this.host.requestUpdate(this.key,!this.matches))}};import{css as L}from"/libs/deps/lit-all.min.js";var c=L` - h2 { - font-size: 11px; - font-style: normal; - font-weight: 500; - height: 32px; - letter-spacing: 0.06em; - padding: 0 12px; - line-height: 32px; - color: #747474; - } -`;import{html as N,LitElement as R}from"/libs/deps/lit-all.min.js";function d(s,e){let t;return function(){let o=this,i=arguments;clearTimeout(t),t=setTimeout(()=>s.apply(o,i),e)}}var x="merch-search:change";var v="merch-sidenav:select";var g="hashchange";function n(s=window.location.hash){let e=[],t=s.replace(/^#/,"").split("&");for(let o of t){let[i,l=""]=o.split("=");i&&e.push([i,decodeURIComponent(l.replace(/\+/g," "))])}return Object.fromEntries(e)}function a(s,e){if(s.deeplink){let t={};t[s.deeplink]=e,A(t)}}function A(s){let e=new URLSearchParams(window.location.hash.slice(1));Object.entries(s).forEach(([i,l])=>{l?e.set(i,l):e.delete(i)}),e.sort();let t=e.toString();if(t===window.location.hash)return;let o=window.scrollY||document.documentElement.scrollTop;window.location.hash=t,window.scrollTo(0,o)}function b(s){let e=()=>{if(window.location.hash&&!window.location.hash.includes("="))return;let t=n(window.location.hash);s(t)};return e(),window.addEventListener(g,e),()=>{window.removeEventListener(g,e)}}var p=class extends R{static properties={deeplink:{type:String}};get search(){return this.querySelector("sp-search")}constructor(){super(),this.handleInput=()=>{a(this,this.search.value),this.search.value&&this.dispatchEvent(new CustomEvent(x,{bubbles:!0,composed:!0,detail:{type:"search",value:this.search.value}}))},this.handleInputDebounced=d(this.handleInput.bind(this))}connectedCallback(){super.connectedCallback(),this.search&&(this.search.addEventListener("input",this.handleInputDebounced),this.search.addEventListener("submit",this.handleInputSubmit),this.updateComplete.then(()=>{this.setStateFromURL()}),this.startDeeplink())}disconnectedCallback(){super.disconnectedCallback(),this.search.removeEventListener("input",this.handleInputDebounced),this.search.removeEventListener("submit",this.handleInputSubmit),this.stopDeeplink?.()}setStateFromURL(){let t=n()[this.deeplink];t&&(this.search.value=t)}startDeeplink(){this.stopDeeplink=b(({search:e})=>{this.search.value=e??""})}handleInputSubmit(e){e.preventDefault()}render(){return N``}};customElements.define("merch-search",p);import{html as C,LitElement as D,css as M}from"/libs/deps/lit-all.min.js";var m=class extends D{static properties={sidenavListTitle:{type:String},label:{type:String},deeplink:{type:String,attribute:"deeplink"},selectedText:{type:String,reflect:!0,attribute:"selected-text"},selectedValue:{type:String,reflect:!0,attribute:"selected-value"}};static styles=[M` - :host { - display: block; - contain: content; - padding-top: 16px; - } - .right { - position: absolute; - right: 0; - } - - ::slotted(sp-sidenav.resources) { - --mod-sidenav-item-background-default-selected: transparent; - --mod-sidenav-content-color-default-selected: var( - --highcontrast-sidenav-content-color-default, - var( - --mod-sidenav-content-color-default, - var(--spectrum-sidenav-content-color-default) - ) - ); - } - `,c];constructor(){super(),this.handleClickDebounced=d(this.handleClick.bind(this))}selectElement(e,t=!0){e.parentNode.tagName==="SP-SIDENAV-ITEM"&&this.selectElement(e.parentNode,!1),e.firstElementChild?.tagName==="SP-SIDENAV-ITEM"&&(e.expanded=!0),t&&(this.selectedElement=e,this.selectedText=e.label,this.selectedValue=e.value,setTimeout(()=>{e.selected=!0},1),this.dispatchEvent(new CustomEvent(v,{bubbles:!0,composed:!0,detail:{type:"sidenav",value:this.selectedValue,elt:this.selectedElement}})))}setStateFromURL(){let t=n()[this.deeplink]??"all";if(t){let o=this.querySelector(`sp-sidenav-item[value="${t}"]`);if(!o)return;this.updateComplete.then(()=>{this.selectElement(o)})}}handleClick({target:e}){let{value:t,parentNode:o}=e;this.selectElement(e),o&&o.tagName==="SP-SIDENAV"&&(a(this,t),e.selected=!0,o.querySelectorAll("sp-sidenav-item[expanded],sp-sidenav-item[selected]").forEach(i=>{i.value!==t&&(i.expanded=!1,i.selected=!1)}))}selectionChanged({target:{value:e,parentNode:t}}){this.selectElement(this.querySelector(`sp-sidenav-item[value="${e}"]`)),a(this,e)}connectedCallback(){super.connectedCallback(),this.addEventListener("click",this.handleClickDebounced),this.updateComplete.then(()=>{this.setStateFromURL()})}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("click",this.handleClickDebounced)}render(){return C`
- ${this.sidenavListTitle?C`

${this.sidenavListTitle}

`:""} - -
`}};customElements.define("merch-sidenav-list",m);import{html as V,LitElement as O,css as I}from"/libs/deps/lit-all.min.js";var u=class extends O{static properties={sidenavCheckboxTitle:{type:String},label:{type:String},deeplink:{type:String},selectedValues:{type:Array,reflect:!0},value:{type:String}};static styles=I` - :host { - display: block; - contain: content; - border-top: 1px solid var(--color-gray-200); - padding: 12px; - } - h3 { - font-size: 14px; - font-style: normal; - font-weight: 700; - height: 32px; - letter-spacing: 0px; - padding: 0px; - line-height: 18.2px; - color: var(--color-gray-600); - margin: 0px; - } - .checkbox-group { - display: flex; - flex-direction: column; - } - `;setStateFromURL(){this.selectedValues=[];let{types:e}=n();e&&(this.selectedValues=e.split(","),this.selectedValues.forEach(t=>{let o=this.querySelector(`sp-checkbox[name=${t}]`);o&&(o.checked=!0)}))}selectionChanged(e){let{target:t}=e,o=t.getAttribute("name");if(o){let i=this.selectedValues.indexOf(o);t.checked&&i===-1?this.selectedValues.push(o):!t.checked&&i>=0&&this.selectedValues.splice(i,1)}a(this,this.selectedValues.join(","))}connectedCallback(){super.connectedCallback(),this.updateComplete.then(async()=>{this.setStateFromURL()})}render(){return V`
-

${this.sidenavCheckboxTitle}

-
- -
-
`}};customElements.define("merch-sidenav-checkbox-group",u);var y="(max-width: 700px)";var S="(max-width: 1199px)";var T=/iP(ad|hone|od)/.test(window?.navigator?.platform)||window?.navigator?.platform==="MacIntel"&&window.navigator.maxTouchPoints>1,E=!1,h,w=s=>{s&&(T?(document.body.style.position="fixed",s.ontouchmove=e=>{e.targetTouches.length===1&&e.stopPropagation()},E||(document.addEventListener("touchmove",e=>e.preventDefault()),E=!0)):(h=document.body.style.overflow,document.body.style.overflow="hidden"))},_=s=>{s&&(T?(s.ontouchstart=null,s.ontouchmove=null,document.body.style.position="",document.removeEventListener("touchmove",e=>e.preventDefault()),E=!1):h!==void 0&&(document.body.style.overflow=h,h=void 0))};var f=class extends P{static properties={sidenavTitle:{type:String},closeText:{type:String,attribute:"close-text"},modal:{type:Boolean,attribute:"modal",reflect:!0}};#e;constructor(){super(),this.modal=!1}static styles=[H` - :host { - display: block; - } - - :host(:not([modal])) { - --mod-sidenav-item-background-default-selected: #222; - --mod-sidenav-content-color-default-selected: #fff; - } - - #content { - width: 100%; - min-width: 300px; - height: 100%; - display: flex; - justify-content: center; - align-items: baseline; - } - - :host([modal]) ::slotted(merch-search) { - display: none; - } - - #sidenav { - display: flex; - flex-direction: column; - max-width: 248px; - overflow-y: auto; - place-items: center; - position: relative; - width: 100%; - padding-bottom: 16px; - } - - sp-dialog-base #sidenav { - padding-top: 16px; - max-width: 300px; - max-height: 80dvh; - min-height: min(500px, 80dvh); - background: #ffffff 0% 0% no-repeat padding-box; - box-shadow: 0px 1px 4px #00000026; - } - - sp-link { - position: absolute; - top: 16px; - right: 16px; - } - `,c];mobileDevice=new r(this,y);mobileAndTablet=new r(this,S);get filters(){return this.querySelector("merch-sidenav-list")}get search(){return this.querySelector("merch-search")}render(){return this.mobileAndTablet.matches?this.asDialog:this.asAside}get asDialog(){if(this.modal)return k` - - -
-
-
-

${this.sidenavTitle}

- -
- ${this.closeText||"Close"} -
-
-
-
- `}get asAside(){return k`

${this.sidenavTitle}

-
`}get dialog(){return this.shadowRoot.querySelector("sp-dialog-base")}closeModal(e){e.preventDefault(),this.dialog?.close(),document.body.classList.remove("merch-modal")}openModal(){this.updateComplete.then(async()=>{w(this.dialog),document.body.classList.add("merch-modal");let e={trigger:this.#e,notImmediatelyClosable:!0,type:"auto"},t=await window.__merch__spectrum_Overlay.open(this.dialog,e);t.addEventListener("close",()=>{this.modal=!1,_(this.dialog)}),this.shadowRoot.querySelector("sp-theme").append(t)})}updated(){this.modal&&this.openModal()}showModal({target:e}){this.#e=e,this.modal=!0}};customElements.define("merch-sidenav",f);export{f as MerchSideNav}; diff --git a/creativecloud/scripts/scripts.js b/creativecloud/scripts/scripts.js index 9855c678a..3b2c01ccb 100644 --- a/creativecloud/scripts/scripts.js +++ b/creativecloud/scripts/scripts.js @@ -132,6 +132,8 @@ const stageDomainsMap = { 'developer.adobe.com': 'developer-stage.adobe.com', 'news.adobe.com': 'news.stage.adobe.com', 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', }, '--cc--adobecom.hlx.live': { 'www.adobe.com': 'origin', @@ -141,6 +143,8 @@ const stageDomainsMap = { 'developer.adobe.com': 'developer-stage.adobe.com', 'news.adobe.com': 'news.stage.adobe.com', 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', }, '--cc--adobecom.hlx.page': { 'www.adobe.com': 'origin', @@ -150,6 +154,8 @@ const stageDomainsMap = { 'developer.adobe.com': 'developer-stage.adobe.com', 'news.adobe.com': 'news.stage.adobe.com', 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', }, }; diff --git a/test/blocks/nonprofit/mocks/body.html b/test/blocks/nonprofit/mocks/body.html new file mode 100644 index 000000000..1857cd327 --- /dev/null +++ b/test/blocks/nonprofit/mocks/body.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/test/blocks/nonprofit/mocks/data.js b/test/blocks/nonprofit/mocks/data.js new file mode 100644 index 000000000..3abfeae15 --- /dev/null +++ b/test/blocks/nonprofit/mocks/data.js @@ -0,0 +1,13 @@ +export const mockedOrganizations = [ + { id: 'atestid_1', name: 'A Test Organization 1' }, + { id: 'atestid_2', name: 'A Test Organization 2' }, + { id: 'atestid_3', name: 'A Test Organization 3' }, + { id: 'atestid_4', name: 'A Test Organization 4' }, +]; + +export const mockedRegistries = [ + { name: 'A Test Registry 1' }, + { name: 'A Test Registry 2' }, + { name: 'A Test Registry 3' }, + { name: 'A Test Registry 4' }, +]; diff --git a/test/blocks/nonprofit/nonprofit.test.js b/test/blocks/nonprofit/nonprofit.test.js new file mode 100644 index 000000000..490046ae2 --- /dev/null +++ b/test/blocks/nonprofit/nonprofit.test.js @@ -0,0 +1,587 @@ +/* eslint-disable max-len */ +/* eslint-disable func-names */ +import sinon from 'sinon'; +import { readFile, sendKeys } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +// import userEvent from '@testing-library/user-event'; +import waitForElement from '../../helpers/waitForElement.js'; + +import { + organizationsStore, + registriesStore, + SCENARIOS, + stepperStore, +} from '../../../creativecloud/blocks/nonprofit/nonprofit.js'; +import { mockedOrganizations, mockedRegistries } from './mocks/data.js'; + +const { default: init } = await import('../../../creativecloud/blocks/nonprofit/nonprofit.js'); + +const body = await readFile({ path: './mocks/body.html' }); + +function delay(ms) { + return new Promise((res) => { setTimeout(() => { res(); }, ms); }); +} + +describe('nonprofit - General', () => { + before(() => { + window.mph = { + 'nonprofit-title-select-non-profit': "What's your nonprofit organization?", + 'nonprofit-title-organization-details': 'Verify your organization details', + 'nonprofit-title-organization-address': "What's the physical address of your organization?", + 'nonprofit-title-personal-details': 'Confirm your details?', + 'nonprofit-subtitle-personal-details': + 'We need to confirm your name and email in order to finish checking if your organisation is eligible.', + 'nonprofit-title-application-review': 'Your application is being reviewed', + }; + }); + + beforeEach(() => { + document.body.innerHTML = body; + const np = document.querySelector('.nonprofit'); + init(np); + }); + + afterEach(() => { + stepperStore.update((prev) => ({ ...prev, step: 1, scenario: SCENARIOS.FOUND_IN_SEARCH })); + }); + + function validateStepTitle(value) { + const title = document.querySelector('.np-title'); + expect(title.textContent).to.equal(value); + } + + it('should display nonprofit', async () => { + const container = await waitForElement('.np-container'); + const form = container.querySelector('.np-form'); + const countryInput = form.querySelector('input[name="country"]'); + const organizationInput = form.querySelector('input[name="organizationId"]'); + expect(container).to.exist; + expect(countryInput).to.exist; + expect(organizationInput).to.exist; + }); + + it('should enable organizations select on country selection', async () => { + const countrySearch = document.querySelector('input[data-for="country"]'); + const organizationSearch = document.querySelector('input[data-for="organizationId"]'); + + expect(organizationSearch.getAttribute('disabled')).to.equal('disabled'); + + countrySearch.focus(); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + expect(organizationSearch.hasAttribute('disabled')).to.be.false; + }); + + it('should change step on stepperStore update', async () => { + validateStepTitle("What's your nonprofit organization?"); + + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + validateStepTitle('Confirm your details?'); + + stepperStore.update({ step: 3, scenario: SCENARIOS.FOUND_IN_SEARCH }); + validateStepTitle('Your application is being reviewed'); + + stepperStore.update({ step: 1, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + validateStepTitle("What's your nonprofit organization?"); + + stepperStore.update({ step: 2, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + validateStepTitle('Verify your organization details'); + + stepperStore.update({ step: 3, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + validateStepTitle("What's the physical address of your organization?"); + + stepperStore.update({ step: 4, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + validateStepTitle('Confirm your details?'); + + stepperStore.update({ step: 5, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + validateStepTitle('Your application is being reviewed'); + }); + + it('should go to previous step on back button click', async () => { + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + validateStepTitle('Confirm your details?'); + + const backButton = document.querySelector('.np-stepper-back'); + + expect(backButton.style.display).to.equal('flex'); + + backButton.click(); + + expect(stepperStore.data.step).to.equal(1); + }); + + it('should go to previous step on back button Enter keypress', async () => { + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + validateStepTitle('Confirm your details?'); + + const backButton = document.querySelector('.np-stepper-back'); + + expect(backButton.style.display).to.equal('flex'); + + backButton.focus(); + await sendKeys({ press: 'Enter' }); + + expect(stepperStore.data.step).to.equal(1); + }); +}); + +describe('nonprofit - Organization search', () => { + const mockedFetchReturnData = { + default: { data: mockedOrganizations, _links: { next: '' } }, + 'should search next organizations': { + data: mockedOrganizations, + _links: { next: 'a.test?link=' }, + }, + 'should navigate to alternate flow': { + data: [], + _links: { next: '' }, + }, + }; + + before(() => { + window.mph = {}; + window.lana = { log: () => {} }; + }); + + beforeEach(async function () { + document.body.innerHTML = body; + const np = document.querySelector('.nonprofit'); + init(np); + + const data = mockedFetchReturnData[this.currentTest.title] || mockedFetchReturnData.default; + + sinon.stub(window, 'fetch'); + window.fetch.returns( + Promise.resolve({ + json: () => Promise.resolve(data), + ok: !data.error, + }), + ); + + const countrySearch = document.querySelector('input[data-for="country"]'); + const organizationSearch = document.querySelector('input[data-for="organizationId"]'); + + countrySearch.focus(); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + expect(document.activeElement).to.equal(organizationSearch); + }); + + afterEach(() => { + if (window.fetch.restore) window.fetch.restore(); + if (window.lana.log.restore) window.lana.log.restore(); + organizationsStore.update([]); + stepperStore.update((prev) => ({ ...prev, step: 1, scenario: SCENARIOS.FOUND_IN_SEARCH })); + }); + + it('should search organizations', async () => { + const selectedOrganizationContainer = document.querySelector( + '.np-selected-organization-container', + ); + + expect(selectedOrganizationContainer.style.display).to.equal(''); + + await sendKeys({ press: 'a' }); + + await waitForElement('.np-select-list[data-for="organizationId"] .np-select-item'); + + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + expect(selectedOrganizationContainer.style.display).to.equal('flex'); + }); + + it('should search next organizations', async () => { + const organizationSearch = document.querySelector('input[data-for="organizationId"]'); + expect(document.activeElement).to.equal(organizationSearch); + + await sendKeys({ press: 'a' }); + + await waitForElement('.np-select-list[data-for="organizationId"] .np-select-item'); + const organizationsList = document.querySelector('.np-select-list[data-for="organizationId"]'); + organizationsList.dispatchEvent(new Event('scroll')); + + expect(window.fetch.getCall(1).args[0]).to.equal('a.test?link='); + }); + + it('should navigate options with arrows', async () => { + await sendKeys({ press: 'a' }); + + const firstOption = await waitForElement( + '.np-select-list[data-for="organizationId"] .np-select-item', + ); + const lastOption = document.querySelector( + '.np-select-list[data-for="organizationId"] .np-select-item:last-child', + ); + + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowDown' }); + + expect(document.activeElement).to.equal(lastOption); + + await sendKeys({ press: 'ArrowUp' }); + await sendKeys({ press: 'ArrowUp' }); + await sendKeys({ press: 'ArrowUp' }); + + expect(document.activeElement).to.equal(firstOption); + }); + + it('should focus the organization search from new keypresses (except arrows and Enter) on select options', async () => { + await sendKeys({ press: 'a' }); + + const firstOption = await waitForElement( + '.np-select-list[data-for="organizationId"] .np-select-item', + ); + + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowUp' }); + + expect(document.activeElement).to.equal(firstOption); + + await sendKeys({ press: 'a' }); + + const organizationSearch = document.querySelector('input[data-for="organizationId"]'); + + expect(document.activeElement).to.equal(organizationSearch); + expect(organizationSearch.value).to.equal('aa'); + }); + + it('should reset select search value on blur without selection', async () => { + await sendKeys({ press: 'a' }); + + const organizationSearch = document.querySelector( + '.np-select-search[data-for="organizationId"]', + ); + + expect(organizationSearch.value).to.equal('a'); + + await waitForElement('.np-select-list[data-for="organizationId"] .np-select-no-options'); + organizationSearch.blur(); + + expect(organizationSearch.value).to.equal(''); + }); + + it('should navigate to personal data', async () => { + const selectedOrganizationContainer = document.querySelector( + '.np-selected-organization-container', + ); + + expect(selectedOrganizationContainer.style.display).to.equal(''); + + await sendKeys({ press: 'a' }); + + await waitForElement('.np-select-list[data-for="organizationId"] .np-select-item'); + + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + expect(selectedOrganizationContainer.style.display).to.equal('flex'); + + const form = document.querySelector('.np-form'); + form.dispatchEvent(new Event('submit')); + + expect(stepperStore.data.step).to.equal(2); + expect(stepperStore.data.scenario).to.equal(SCENARIOS.FOUND_IN_SEARCH); + }); + + it('should navigate to alternate flow', async () => { + await sendKeys({ press: 'a' }); + + await waitForElement('.np-select-list[data-for="organizationId"] .np-select-no-options'); + + await sendKeys({ press: 'Tab' }); + + const cannotFind = document.querySelector('.np-organization-cannot-find a'); + expect(document.activeElement).to.equal(cannotFind); + + await sendKeys({ press: 'Enter' }); + + expect(stepperStore.data.step).to.equal(2); + expect(stepperStore.data.scenario).to.equal(SCENARIOS.NOT_FOUND_IN_SEARCH); + }); +}); + +describe('nonprofit - Organization details', () => { + const mockedFetchReturnData = { + default: { data: mockedRegistries }, + 'should search organizations': { data: mockedRegistries }, + }; + + before(() => { + window.mph = {}; + }); + + beforeEach(async function () { + document.body.innerHTML = body; + const np = document.querySelector('.nonprofit'); + init(np); + + const data = mockedFetchReturnData[this.currentTest.title] || mockedFetchReturnData.default; + + sinon.stub(window, 'fetch'); + window.fetch.returns( + Promise.resolve({ + json: () => Promise.resolve(data), + ok: true, + }), + ); + + stepperStore.update({ step: 2, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + }); + + afterEach(() => { + if (window.fetch.restore) window.fetch.restore(); + registriesStore.update([]); + }); + + it('should fetch registries on country select', async () => { + const countrySearch = document.querySelector('.np-select-search[data-for="country"]'); + + countrySearch.focus(); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + await waitForElement('.np-select-list[data-for="registry"] .np-select-item'); + + expect(window.fetch.getCall(0)).to.not.be.null; + }); + + it('should not navigate without completed fields', async () => { + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(2); + }); + + // userEvent crashes on import - this test is skipped + it.skip('should navigate to address details on submit', async () => { + const countrySearch = document.querySelector('.np-select-search[data-for="country"]'); + + countrySearch.focus(); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + await waitForElement('.np-select-list[data-for="registry"] .np-select-item'); + + const registrySearch = document.querySelector('.np-select-search[data-for="registry"]'); + + registrySearch.focus(); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'Enter' }); + + const organizationName = document.querySelector('.np-input[name="organizationName"]'); + organizationName.value = 'A Test Name'; + organizationName.dispatchEvent(new Event('input')); + + const organizationRegistrationId = document.querySelector( + '.np-input[name="organizationRegistrationId"]', + ); + organizationRegistrationId.value = 'atestid'; + organizationRegistrationId.dispatchEvent(new Event('input')); + + // const evidenceNonProfitStatus = document.querySelector( + // '.np-input[name="evidenceNonProfitStatus"]', + // ); + // const file = new File(['evidenceofnonprofit'], 'evidenceofnonprofit.png', { type: 'image/png' }); + // await userEvent.upload(evidenceNonProfitStatus, file); + + const website = document.querySelector('.np-input[name="website"]'); + website.value = 'www.atestwebsite.com'; + website.dispatchEvent(new Event('input')); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(3); + }); +}); + +describe('nonprofit - Address details', () => { + before(() => { + window.mph = {}; + }); + + beforeEach(async () => { + document.body.innerHTML = body; + const np = document.querySelector('.nonprofit'); + init(np); + + stepperStore.update({ step: 3, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + }); + + it('should not navigate without completed fields', async () => { + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(3); + }); + + it('should navigate to personal details on submit with all fields completed', async () => { + const streetAddress = document.querySelector('.np-input[name="streetAddress"]'); + streetAddress.value = 'A Test Street, 40'; + streetAddress.dispatchEvent(new Event('input')); + + const addressDetails = document.querySelector('.np-input[name="addressDetails"]'); + addressDetails.value = 'Block C, Floor 2'; + addressDetails.dispatchEvent(new Event('input')); + + const state = document.querySelector('.np-input[name="state"]'); + state.value = 'A Test State'; + state.dispatchEvent(new Event('input')); + + const city = document.querySelector('.np-input[name="city"]'); + city.value = 'A Test City'; + city.dispatchEvent(new Event('input')); + + const zipCode = document.querySelector('.np-input[name="zipCode"]'); + zipCode.value = 'TEST-0123'; + zipCode.dispatchEvent(new Event('input')); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(4); + }); + + it('should navigate to personal details on submit with just the required fields completed', async () => { + const streetAddress = document.querySelector('.np-input[name="streetAddress"]'); + streetAddress.value = 'A Test Street, 40'; + streetAddress.dispatchEvent(new Event('input')); + + const city = document.querySelector('.np-input[name="city"]'); + city.value = 'A Test City'; + city.dispatchEvent(new Event('input')); + + const zipCode = document.querySelector('.np-input[name="zipCode"]'); + zipCode.value = 'TEST-0123'; + zipCode.dispatchEvent(new Event('input')); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(4); + }); +}); + +describe('nonprofit - Personal details', () => { + const fillFields = () => { + const firstName = document.querySelector('.np-input[name="firstName"]'); + firstName.value = 'Atest'; + firstName.dispatchEvent(new Event('input')); + + const lastName = document.querySelector('.np-input[name="lastName"]'); + lastName.value = 'Name'; + lastName.dispatchEvent(new Event('input')); + + const email = document.querySelector('.np-input[name="email"]'); + email.value = 'atest@email.com'; + email.dispatchEvent(new Event('input')); + }; + + before(() => { + window.mph = {}; + window.lana = { log: () => {} }; + }); + + beforeEach(async () => { + document.body.innerHTML = body; + const np = document.querySelector('.nonprofit'); + init(np); + }); + + afterEach(() => { + if (window.fetch.restore) window.fetch.restore(); + if (window.lana.log.restore) window.lana.log.restore(); + }); + + it('should not submit without completed fields', async () => { + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + expect(stepperStore.data.step).to.equal(2); + }); + + it('should submit completed form in found in search scenario', async () => { + sinon.stub(window, 'fetch'); + window.fetch.returns( + Promise.resolve({ + json: () => Promise.resolve({ data: { validationInviteId: 'avalidationinviteid_0123' } }), + ok: true, + }), + ); + + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + + fillFields(); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + await waitForElement('.np-application-review-container'); + + expect(stepperStore.data.step).to.equal(3); + }); + + it('should submit completed form in not found in search scenario', async () => { + sinon.stub(window, 'fetch'); + window.fetch.returns( + Promise.resolve({ + json: () => Promise.resolve({ data: { validationInviteId: 'avalidationinviteid_0123' } }), + ok: true, + }), + ); + + stepperStore.update({ step: 4, scenario: SCENARIOS.NOT_FOUND_IN_SEARCH }); + + fillFields(); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + await waitForElement('.np-application-review-container'); + + expect(stepperStore.data.step).to.equal(5); + }); + + it('should not navigate on submission failure', async () => { + sinon.stub(window, 'fetch'); + sinon.stub(window.lana, 'log'); + + window.fetch.returns( + Promise.resolve({ + json: () => Promise.resolve({ error: { title: 'An error title', message: 'A submission failure' } }), + ok: false, + }), + ); + + stepperStore.update({ step: 2, scenario: SCENARIOS.FOUND_IN_SEARCH }); + + fillFields(); + + const submit = document.querySelector('input[type="submit"]'); + expect(submit).to.exist; + submit.click(); + + await delay(500); + + expect(stepperStore.data.step).to.equal(2); + expect(window.lana.log.getCall(0).args[0]).to.equal( + 'Could not send organization data: Error: An error title: A submission failure', + ); + }); +}); diff --git a/test/blocks/sidenav/sidenav.test.html.js b/test/blocks/sidenav/sidenav.test.html.js index ca6aeba1c..cdf267e43 100644 --- a/test/blocks/sidenav/sidenav.test.html.js +++ b/test/blocks/sidenav/sidenav.test.html.js @@ -4,7 +4,7 @@ import { expect } from '@esm-bundle/chai'; import sinon from 'sinon'; import { setLibs } from '../../../creativecloud/scripts/utils.js'; -setLibs('/node_modules/@adobecom/milo/libs', true); +setLibs('https://milo.adobe.com/libs', true); const { default: init } = await import('../../../creativecloud/blocks/sidenav/sidenav.js');