UNPKG

@spectrum-css/icon

Version:
284 lines (254 loc) 7.52 kB
import { getRandomId } from "@spectrum-css/preview/decorators"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import "../index.css"; /** * @typedef { keyof import("./icon.stories.js").default.args } IconArgs * @typedef { IconArgs & { scale: string, setName: 'workflow' | 'ui' } } IconProps */ const fetchIconDetails = ({ icons, workflowIcons = [], uiIcons = [], uiIconSizes = {}, }) => { if (!icons || Object.keys(icons).length == 0) { // Fetch loaded data if not provided if (window.icons) icons = window.icons; else { return { workflowIcons: [], uiIcons: [], uiIconSizes: {}, uiIconsWithDirections: [], }; } } // clean up loaded icon data icons = Object.entries(icons).reduce((acc, [setName, data]) => { acc[setName] = Object.entries(data).reduce((acc, [size, data]) => { acc[size] = Object.entries(data).reduce((acc, [iconName, svg]) => { // simplify icon name iconName = iconName.split("/").pop().replace(/\.svg$/, ""); acc[iconName] = svg; // Add the icon name to the workflowIcons list if it's from the workflow set if (setName === "workflow") { workflowIcons.push(iconName); } else { const iconNameRoot = iconName.replace(/\d{2,3}$/, "").replace(/(Right|Left|Down|Up)$/, ""); const iconNameSize = iconName.match(/\d{2,3}/g)?.[0]; uiIcons.push(iconNameRoot); uiIconSizes[iconNameRoot] = [ ...new Set([ ...uiIconSizes[iconNameRoot] ?? [], ...(iconNameSize ? [iconNameSize] : []), ]) ]; } return acc; }, {}); return acc; }, {}); return acc; }, {}); return { icons, workflowIcons: [...new Set(workflowIcons)], uiIcons: [...new Set(uiIcons)], uiIconSizes, uiIconsWithDirections: [ ...new Set([ ...(uiIcons.filter((c) => !["Chevron", "Arrow"].includes(c)) ?? []), "ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown", "ChevronRight", "ChevronLeft", "ChevronUp", "ChevronDown", ]) ], }; }; /** * Template for rendering an icon * @description Icon template that renders an icon based on the provided icon name and set name. * @param {IconProps} props * @param {string} props.rootClass * @param {"xs"|"s"|"m"|"l"|"xl"|"xxl"} props.size * @param {"ui"|"workflow"} props.setName * @param {string} props.iconName Icon name; could be from either icon set. * @param {string} props.uiIconName Icon name selected from the UI icon set. When defined, takes precedence over iconName when setName == "ui". * @param {string} props.fill * @param {string} props.id * @param {string[]} props.customClasses * @returns {import('lit').TemplateResult<1>} */ export const Template = ({ rootClass = "spectrum-Icon", size = "m", setName, iconName, uiIconName, fill, id = getRandomId("icon"), customClasses = [], icons, useRef = true, workflowIcons, uiIcons, uiIconSizes, } = {}, context = {}) => { const { globals = {}, loaded = {} } = context; const scale = globals.scale ?? "medium"; if (!workflowIcons || !uiIcons || !uiIconSizes) { const details = fetchIconDetails({ icons: loaded.icons, workflowIcons, uiIcons, uiIconSizes }); if(details.icons) { icons = details.icons; } if (!workflowIcons && details.workflowIcons) { workflowIcons = details.workflowIcons; } if (!uiIcons && details.uiIcons) { uiIcons = details.uiIcons; } if (!uiIconSizes && details.uiIconSizes) { uiIconSizes = details.uiIconSizes; } } // UI icons are selected from a different control. if (setName === "ui" && uiIconName) { iconName = uiIconName; } if (!iconName) { console.warn( "Icon: Could not render a result because no icon name was provided to the icon template." ); return html``; } let idKey = iconName; // If icon set was not provided, try determine which icon set contains this icon. // Note: icon sets can contain the same icon name, with different icons. if (!["workflow","ui"].includes(setName)) { if (workflowIcons.includes(idKey)) { setName = "workflow"; } else if (uiIcons.includes(idKey.replace(/\d{2,3}$/, "").replace(/(Right|Left|Down|Up)$/, ""))) { setName = "ui"; } } // Prefer full SVG for UI icons because sizing is more consistent. if (setName === "ui" && uiIconName) { useRef = false; } if (!setName) { console.warn( `Icon: Could not determine the icon set for the provided icon name: ${idKey}.` ); return html``; } // If a descriptor like Right, Left, Down, or Up is present for the UI icons Chevron or // Arrow, use that only for the class and not the icon fetch. if ( setName == "ui" && uiIcons.some((c) => idKey.startsWith(c)) && ["Right", "Left", "Down", "Up"].some((c) => idKey.includes(c)) ) { idKey = idKey.replace(/(Right|Left|Down|Up)/, ""); } /** * Fallback UI Icon sizing number. * * If the icon name includes its scale, we want to leave that scale. This is preferred, * as UI icons do not use workflow icon sizing. * * If the UI icon name does not include scale, reformat it to match the provided sizing. * E.g. with a size of "s", the icon name "ChevronRight" would become "ChevronRight75". */ if ( setName == "ui" && // Exists in the list of available UI icons. uiIcons.includes(idKey.replace(/\d{2,3}$/, "")) && // Does not already have size number at the end. !idKey.match(/^(?!\d).*\d{2,3}$/) && // Exclude some UI icons that do not (yet) have size numbers. uiIconSizes[idKey]?.length != 0 ) { let sizeVal; switch (size) { case "xs": case "s": sizeVal = "75"; break; case "l": sizeVal = "200"; break; case "xl": case "xxl": sizeVal = "300"; break; default: sizeVal = "100"; break; } idKey += sizeVal; iconName += sizeVal; } // Fetch SVG file markup, and set optional fill color. let inlineStyle; if (fill) inlineStyle = `color: ${fill}`; let svgString; if (!useRef && icons && icons[setName]?.[scale]?.[idKey]) { svgString = icons[setName][scale][idKey]; } /** * Classes to apply to the SVG element. Object as used by the classMap function. * @type {[name: string]: string | boolean | number} */ const classList = { [rootClass]: true, [`spectrum-UIIcon-${iconName}`]: !!(setName === "ui"), [`${rootClass}--${scale}`]: !!(setName === "ui" && scale), [`${rootClass}--size${size?.toUpperCase()}`]: !!( (!setName || setName === "workflow") && size ), ...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}), }; if (svgString) { const classesAsString = Object.entries(classList).reduce((acc, [key, value]) => { if (value) acc += `${key} `; return acc; }, ""); return html`${unsafeSVG( svgString.replace(/<svg/, `<svg class="${classesAsString}" focusable="false" aria-hidden="true" role="img"`) )}`; } // ui ID: #spectrum-css-icon-${idKey} // workflow ID: #spectrum-icon-(18|24)-${idKey} const iconID = setName !== "workflow" ? `spectrum-css-icon-${idKey}` : `spectrum-icon-${scale !== "medium" ? "24" : "18"}-${idKey}`; return html`<svg class=${classMap(classList)} id=${ifDefined(id)} style=${ifDefined(inlineStyle)} focusable="false" aria-hidden="true" aria-labelledby=${idKey} role="img" > <title id=${idKey}>${idKey.replace(/([A-Z])/g, " $1").trim()}</title> <use xlink:href="#${iconID}" href="#${iconID}" /> </svg>`; };