8000 [MWPW-159581] Federal icons by robert-bogos · Pull Request #3797 · adobecom/milo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[MWPW-159581] Federal icons #3797

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

Open
wants to merge 17 commits into
base: stage
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion libs/blocks/library-config/library-config.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ input.sk-library-search-input:focus {
* Margin height equal to search bar height
*/
.sk-library ul.con-blocks-list.inset,
.sk-library ul.con-templates-list.inset {
.sk-library ul.con-templates-list.inset,
.sk-library ul.con-icons-list.inset {
margin-bottom: 39px;
}

Expand Down Expand Up @@ -244,6 +245,7 @@ input.sk-library-search-input:focus {
}

.sk-library .block-group.is-hidden,
.sk-library .icon-item.is-hidden,
.con-templates-list .template.is-hidden {
display: none;
}
Expand Down Expand Up @@ -290,6 +292,10 @@ input.sk-library-search-input:focus {
padding: 0 12px;
}

.sk-library li.icon-item img.icon-fed {
filter: invert(100%);
}

.sk-library li.template {
display: flex;
align-items: stretch;
Expand Down
13 changes: 10 additions & 3 deletions libs/blocks/library-config/library-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ async function loadPlaceholders(content, list) {
placeholders(content, list);
}

async function loadIcons(content, list) {
async function loadIcons({ content, list, query }) {
const { default: icons } = await import('./lists/icons.js');
icons(content, list);
icons(content, list, query);
}

async function loadAssets(content, list) {
Expand Down Expand Up @@ -56,6 +56,9 @@ function addSearch({ content, list, type }) {
case 'templates':
loadTemplates({ content, list, query, type });
break;
case 'icons':
loadIcons({ content, list, query });
break;
default:
}
});
Expand All @@ -71,6 +74,9 @@ function addSearch({ content, list, type }) {
case 'templates':
loadTemplates({ content, list, query, type });
break;
case 'icons':
loadIcons({ content, list, query });
break;
default:
}
});
Expand Down Expand Up @@ -98,7 +104,8 @@ async function loadList(type, content, list) {
loadPlaceholders(content, list);
break;
case 'icons':
loadIcons(content, list);
addSearch({ content, list, type });
loadIcons({ content, list });
break;
case 'assets':
loadAssets(content, list);
Expand Down
54 changes: 34 additions & 20 deletions libs/blocks/library-config/lists/icons.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import { fetchIcons } from '../../../features/icons/icons.js';
import { createTag, getConfig } from '../../../utils/utils.js';
import { fetchIconList } from '../../../features/icons/icons.js';
import { createTag } from '../../../utils/utils.js';
import createCopy from '../library-utils.js';

export default async function iconList(content, list) {
const config = getConfig();
const icons = await fetchIcons(config);
Object.keys(icons).forEach((key) => {
const icon = createTag('span', { class: `icon icon-${key}` }, icons[key]);
const titleText = createTag('p', { class: 'item-title' }, key);
const title = createTag('li', { class: 'icon-item' }, icon);
title.append(titleText) 8000 ;
const copy = createTag('button', { class: 'copy' });
copy.id = `${key}-icon-copy`;
copy.addEventListener('click', (e) => {
e.target.classList.add('copied');
setTimeout(() => { e.target.classList.remove('copied'); }, 3000);
const formatted = `:${key}:`;
const blob = new Blob([formatted], { type: 'text/plain' });
createCopy(blob);
let fedIconList;
const iconElements = new Map();

export default async function iconList(content, list, query) {
if (!fedIconList) {
fedIconList = await fetchIconList(content[0].path);
if (!fedIconList?.length) throw new Error('No icons returned from fetchIconList');
fedIconList.forEach((icon) => {
const svg = createTag('span', { class: `icon icon-${icon.name}` }, createTag('img', { class: `icon-${icon.name}-img icon-fed`, src: `${icon.url}`, width: '18px' }));
const titleText = createTag('p', { class: 'item-title' }, icon.name);
const title = createTag('li', { class: 'icon-item' }, svg);
title.append(titleText);
const copy = createTag('button', { class: 'copy' });
copy.id = `${icon.name}-icon-copy`;
copy.addEventListener('click', (e) => {
e.target.classList.add('copied');
setTimeout(() => { e.target.classList.remove('copied'); }, 3000);
const formatted = `:${icon.name}:`;
const blob = new Blob([formatted], { type: 'text/plain' });
createCopy(blob);
window.hlx?.rum.sampleRUM('click', { source: e.target });
});
title.append(copy);

iconElements.set(icon.name, title);
});
title.append(copy);
list.append(title);
}

list.replaceChildren();

iconElements.forEach((element, name) => {
element.classList.toggle('is-hidden', query && !name.includes(query));
list.appendChild(element);
});
}
192 changes: 135 additions & 57 deletions libs/features/icons/icons.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
let fetchedIcons;
let fetched = false;
import { lanaLog } from '../../blocks/global-navigation/utilities/utilities.js';
import { getFederatedContentRoot } from '../../utils/federated.js';
import { getConfig } from '../../utils/utils.js';

async function getSVGsfromFile(path) {
/* c8 ignore next */
if (!path) return null;
const resp = await fetch(path);
/* c8 ignore next */
if (!resp.ok) return null;
const miloIcons = {};
const text = await resp.text();
const parser = new DOMParser();
const parsedText = parser.parseFromString(text, 'image/svg+xml');
const symbols = parsedText.querySelectorAll('symbol');
symbols.forEach((symbol) => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
while (symbol.firstChild) svg.appendChild(symbol.firstChild);
[...symbol.attributes].forEach((attr) => svg.attributes.setNamedItem(attr.cloneNode()));
svg.classList.add('icon-milo', `icon-milo-${svg.id}`);
miloIcons[svg.id] = svg;
});
return miloIcons;
}

// eslint-disable-next-line no-async-promise-executor
export const fetchIcons = (config) => new Promise(async (resolve) => {
/* c8 ignore next */
if (!fetched) {
const { miloLibs, codeRoot } = config;
const base = miloLibs || codeRoot;
fetchedIcons = await getSVGsfromFile(`${base}/img/icons/icons.svg`);
fetched = true;
}
resolve(fetchedIcons);
});
const iconCache = new Map();
let miloIconsPromise;

let tooltipListenersAdded = false;
function addTooltipListeners() {
Expand All @@ -59,44 +30,151 @@ function decorateToolTip(icon, iconName) {

const wrapper = icon.closest('em');
wrapper.className = 'tooltip-wrapper';
if (!wrapper) return;
const conf = wrapper.textContent.split('|');
// Text is the last part of a tooltip
const content = conf.pop().trim();
const content = conf.pop()?.trim();
if (!content) return;

icon.dataset.tooltip = content;
// Position is the next to last part of a tooltip
const place = conf.pop()?.trim().toLowerCase() || 'right';
icon.className = `icon icon-${iconName} milo-tooltip ${place}`;
icon.setAttribute('tabindex', '0');
icon.setAttribute('aria-label', content);
icon.setAttribute('role', 'button');

[['tabindex', '0'], ['aria-label', content], ['role', 'button']].forEach(([attr, value]) => {
icon.setAttribute(attr, value);
});

wrapper.parentElement.replaceChild(icon, wrapper);
if (!tooltipListenersAdded) addTooltipListeners();
}

export default async function loadIcons(icons, config) {
const iconSVGs = await fetchIcons(config);
if (!iconSVGs) return;
icons.forEach(async (icon) => {
async function getSVGsfromFile(path) {
if (!path) return null;
const resp = await fetch(path);
if (!resp.ok) return null;

const miloIcons = {};
const text = await resp.text();
const parser = new DOMParser();
const parsedText = parser.parseFromString(text, 'image/svg+xml');
const symbols = parsedText.querySelectorAll('symbol');

symbols.forEach((symbol) => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
while (symbol.firstChild) svg.appendChild(symbol.firstChild);
[...symbol.attributes].forEach((attr) => svg.attributes.setNamedItem(attr.cloneNode()));
svg.classList.add('icon-milo', `icon-milo-${svg.id}`);
miloIcons[svg.id] = svg;
});

return miloIcons;
}

async function fetchAndParseSVG(url, iconName) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch SVG for ${iconName}: ${response.statusText}`);

const text = await response.text();
const parser = new DOMParser();
const svgElement = parser.parseFromString(text, 'image/svg+xml').querySelector('svg');

if (!svgElement) throw new Error(`No SVG element found in fetched content for ${iconName}`);

svgElement.classList.add('icon-milo', `icon-milo-${iconName}`);
return svgElement;
}

async function fetchFederalIcon(iconName) {
const fedRoot = getFederatedContentRoot();
const url = `${fedRoot}/federal/assets/icons/svgs/${iconName}.svg`;

try {
const svgElement = await fetchAndParseSVG(url, iconName);
iconCache.set(iconName, svgElement);
return svgElement;
} catch (error) {
lanaLog({
message: `Error fetching federal SVG for ${iconName}, falling back to Milo icon`,
error,
tags: 'icons',
errorType: 'error',
});
return null;
}
}

async function fetchMiloIcon(iconName) {
if (!miloIconsPromise) {
const { miloLibs, codeRoot } = getConfig();
const base = miloLibs || codeRoot;
miloIconsPromise = getSVGsfromFile(`${base}/img/icons/icons.svg`);
}

const miloIcons = await miloIconsPromise;
if (miloIcons?.[iconName]) {
const icon = miloIcons[iconName].cloneNode(true);
iconCache.set(iconName, icon);
return icon;
}

lanaLog({
message: `No fallback Milo icon found for ${iconName}`,
tags: 'icons',
errorType: 'error',
});
return null;
}

async function getIcon(iconName) {
if (iconCache.has(iconName)) return iconCache.get(iconName);

const federalIcon = await fetchFederalIcon(iconName);
if (federalIcon) return federalIcon;

return fetchMiloIcon(iconName);
}

export default async function loadIcons(icons) {
const iconPromises = [...icons].map(async (icon) => {
const iconNameInitial = icon.classList[1].replace('icon-', '');
let iconName = iconNameInitial === 'tooltip' ? 'info' : iconNameInitial;
if (iconNameInitial.includes('tooltip-')) iconName = iconNameInitial.replace(/tooltip-/, '');
decorateToolTip(icon, iconName);
if (icon.dataset.svgInjected || !iconName) return;

const svgElement = await getIcon(iconName);
if (svgElement && !icon.dataset.svgInjected) {
const svgClone = svgElement.cloneNode(true);
icon.appendChild(svgClone);
icon.dataset.svgInjected = 'true';

const existingIcon = icon.querySelector('svg');
if (!iconSVGs[iconName] || existingIcon) return;
const parent = icon.parentElement;
if (parent?.childNodes.length > 1) {
if (parent.lastChild === icon) {
icon.classList.add('margin-inline-start');
} else if (parent.firstChild === icon) {
icon.classList.add('margin-inline-end');
if (parent.parentElement.tagName === 'LI') parent.parentElement.classList.add('icon-list-item');
} else {
icon.classList.add('margin-inline-start', 'margin-inline-end');
const parent = icon.parentElement;
if (parent?.childNodes.length > 1) {
if (parent.lastChild === icon) {
icon.classList.add('margin-inline-start');
} else if (parent.firstChild === icon) {
icon.classList.add('margin-inline-end');
if (parent.parentElement.tagName === 'LI') parent.parentElement.classList.add('icon-list-item');
} else {
icon.classList.add('margin-inline-start', 'margin-inline-end');
}
}
}
icon.insertAdjacentHTML('afterbegin', iconSVGs[iconName].outerHTML);
});

await Promise.allSettled(iconPromises);
}

export const fetchIcons = (config) => {
const { miloLibs, codeRoot } = config;
const base = miloLibs || codeRoot;
return getSVGsfromFile(`${base}/img/icons/icons.svg`);
};

export function fetchIconList(url) {
return fetch(url)
.then((resp) => resp.json())
.then((json) => json.content.data)
.catch(() => {
lanaLog({ message: 'Failed to fetch iconList', tags: 'icons', errorType: 'error' });
return [];
});
}
2 changes: 1 addition & 1 deletion libs/styles/styles.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*** Variables ***/

:root {
:root {
--global-height-nav: 64px;
--feds-height-nav: 63px;
--global-height-breadcrumbs: 33px;
Expand Down
1 change: 0 additions & 1 deletion libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,6 @@ async function decorateIcons(area, config) {
if (icons.length === 0) return;
const { base } = config;
loadStyle(`${base}/features/icons/icons.css`);
loadLink(`${base}/img/icons/icons.svg`, { rel: 'preload', as: 'fetch', crossorigin: 'anonymous' });
const { default: loadIcons } = await import('../features/icons/icons.js');
await loadIcons(icons, config);
}
Expand Down
Loading
Loading
0