Skip to content
Merged
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 components/actionbutton/stories/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ export const Template = ({
${when(hasPopup && hasPopup !== "false", () =>
Icon({
size,
iconName: "CornerTriangle",
iconName: "CornerTriangle" + ({
xs: "75",
s: "75",
m: "100",
l: "200",
xl: "300",
}[size] || "100"),
setName: "ui",
customClasses: [`${rootClass}-hold`],
}, context)
Expand Down
4 changes: 2 additions & 2 deletions components/button/stories/button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ const ButtonIconGroup = (args, context) => Container({
testHeading: "UI icon (larger)",
content: Template({
...args,
// UI icon that is larger than workflow sizing:
iconName: "ArrowDown600",
// Largest UI icon from UI icon set:
iconName: "Cross600",
iconSet: "ui",
}, context),
},
Expand Down
8 changes: 8 additions & 0 deletions components/icon/stories/icon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ with:

<Canvas of={IconStories.UIArrows} />

## Missing workflow icon placeholder

In Storybook documentation, if a workflow icon name does not exist in the set, the placeholder "Circle" icon
will be shown. Missing ui icons will render nothing. The following example purposefully uses an
icon name that does not exist to demonstrate this behavior.

<Canvas of={IconStories.MissingWorkflowIcon} />

## Repositories for the icon SVG files

The UI icon SVGs are within the Spectrum CSS repository, which has its own package published to NPM:
Expand Down
17 changes: 17 additions & 0 deletions components/icon/stories/icon.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,20 @@ UIArrows.tags = ["!dev"];
UIArrows.parameters = {
chromatic: { disableSnapshot: true },
};

/**
* In Storybook documentation, if a workflow icon name does not exist in the set, the
* placeholder "Circle" icon will be shown. Missing ui icons will render
* nothing. The following example purposefully uses an icon name that does
* not exist to demonstrate this behavior.
*/
export const MissingWorkflowIcon = Default.bind({});
MissingWorkflowIcon.storyName = "Missing workflow icon placeholder";
MissingWorkflowIcon.tags = ["!dev"];
MissingWorkflowIcon.args = {
setName: "workflow",
iconName: "ThisIconNameDoesNotExist",
};
MissingWorkflowIcon.parameters = {
chromatic: { disableSnapshot: true },
};
142 changes: 41 additions & 101 deletions components/icon/stories/template.js
Comment thread
jawinn marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { styleMap } from "lit/directives/style-map.js";
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import { when } from "lit/directives/when.js";
import { getSpriteSheetName, uiIconsCleaned, uiIconsWithDirections, workflowIconsCleaned } from "./utilities.js";
import { appendUiIconDefaultSizing, getSpriteSheetName, uiIconsWithDirections, workflowIconsCleaned } from "./utilities.js";

import "../index.css";

Expand All @@ -33,7 +33,7 @@ import "../index.css";
export const Template = ({
rootClass = "spectrum-Icon",
size = "m",
setName,
setName = "workflow",
iconName,
uiIconName,
fill,
Expand All @@ -50,113 +50,53 @@ export const Template = ({
iconName = uiIconName;
}

// Make sure icon name is provided.
if (!iconName) {
// Make sure icon set is provided.
if (!["ui","workflow"].includes(setName)) {
console.warn(
"Icon: Could not render a result because no icon name was provided to the icon template."
`Icon "${iconName}" is missing its icon set. Make sure you are explicitly setting either the workflow or ui icon set.`
);
return html``;
}

// Name of icon that corresponds with SVG file. This may differ from the icon name, such as with
// directional icons that use a single icon.
let idKey = iconName;

// If a descriptor like "Right", "Left", "Down", or "Up" is present for the UI icons Chevron or
// Arrow, use that only for the class name and not the icon fetch. This is because these use a
// single icon file that is rotated in CSS.
if (
["Right", "Left", "Down", "Up"].some((c) => idKey.includes(c)) &&
setName === "ui"
) {
idKey = idKey.replace(/(Right|Left|Down|Up)/, "");
}

// Make sure icon set is provided.
if (!setName) {
console.warn(
`Icon "${idKey}" is missing its icon set. Make sure you are explicitly setting either the workflow or ui icon set.`
);
// Make sure icon name is provided.
if (!iconName) {
console.warn("Icon: Could not render a result because no icon name was provided to the icon template.");
return html``;
}

/**
* 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.
* Append approximate sizing number to UI icons passed in without a sizing number.
*
* If the UI icon name does not include scale, or the scale does not exist in the current
* list of UI icons, reformat it to approximate the provided sizing for the component.
* Note: It's preferred for components to provide the specific UI sizing numbers in the UI icon
* name, rather than relying on this approximation, as UI icons do not use t-shirt sizing.
*/
if (
setName === "ui" &&
(
// Does not already have size number at the end.
!idKey.match(/\d{2,3}$/) ||
// If the provided icon name includes the sizing number, make sure it's a supported sizing number;
// if not, strip it from the key.
(
idKey.match(/\d{2,3}$/) &&
!uiIconsCleaned.includes(idKey)
)
)
) {
let sizeVal;
switch (size) {
case "xs":
if (["CornerTriangle", "Cross"].some(c => idKey.startsWith(c))) {
sizeVal = "75";
}
else if (["Arrow", "Asterisk", "LinkOut"].some(c => idKey.startsWith(c))) {
sizeVal = "100";
}
else {
sizeVal = "50";
}
break;
case "s":
if (["Arrow", "Asterisk", "LinkOut"].some(c => idKey.startsWith(c))) {
sizeVal = "100";
}
else {
sizeVal = "75";
}
break;
case "l":
if (["Arrow"].some(c => idKey.startsWith(c))) {
sizeVal = "400";
}
else {
sizeVal = "200";
}
break;
case "xl":
case "xxl":
if (["Arrow"].some(c => idKey.startsWith(c))) {
sizeVal = "400";
}
else {
sizeVal = "300";
}
break;
default:
sizeVal = "100";
break;
}
if (setName === "ui") {
iconName = appendUiIconDefaultSizing(iconName, size);
}

console.warn(`Using fallback UI Icon sizing number "${sizeVal}" for "${idKey}". UI icon size was not provided or does not exist in the list of available UI icons.`);
// Make sure icon exists in the set.
if (setName == "ui" && !uiIconsWithDirections.includes(iconName)) {
console.warn(`Icon: Could not render an icon with the name "${iconName}" because it does not exist in the "ui" icon set.`);
return html``;
}

// Replace sizing number on idKey and iconName with new fallback size.
idKey = idKey.replace(/\d{2,3}$/, "");
idKey += sizeVal;
if (setName == "workflow" && !workflowIconsCleaned.includes(iconName)) {
console.warn(`Icon: Could not render the correct icon with the name "${iconName}" because it does not exist in the "workflow" icon set. Rendering the placeholder icon instead.`);
iconName = "Circle";
}

iconName = iconName.replace(/\d{2,3}$/, "");
iconName += sizeVal;
// Name of icon that corresponds with SVG file. This may differ from the icon name, such as with
// directional icons that use a single icon.
let iconNameToLoad = iconName;

if (!uiIconsCleaned.includes(idKey)) {
console.error(`The UI icon "${idKey}" does not exist in the list of available UI icons.`);
}
// If a descriptor like "Right", "Left", "Down", or "Up" is present for the UI icons Chevron or
// Arrow, use that only for the class name and not the icon fetch. This is because these use a
// single icon file that is rotated in CSS.
if (
["Right", "Left", "Down", "Up"].some((c) => iconNameToLoad.includes(c)) &&
setName === "ui"
) {
iconNameToLoad = iconNameToLoad.replace(/(Right|Left|Down|Up)/, "");
}

/**
Expand All @@ -178,8 +118,8 @@ export const Template = ({
*/
if (!useRef) {
let svgString;
if (loaded?.icons && loaded?.icons[setName]?.[idKey]) {
svgString = loaded.icons[setName][idKey];
if (loaded?.icons && loaded?.icons[setName]?.[iconNameToLoad]) {
svgString = loaded.icons[setName][iconNameToLoad];
}

// Return the individual SVG's entire markup.
Expand All @@ -194,12 +134,12 @@ export const Template = ({
)}`;
}
else {
console.warn(`Could not find SVG markup for "${idKey}" in context.loaded.icons. Did you pass through context? Falling back to using the sprite sheet reference instead.`);
console.warn(`Could not find SVG markup for "${iconNameToLoad}" in context.loaded.icons. Was context passed through in the template? Falling back to using the sprite sheet reference instead.`);
}
}

// ID of the icon within the sprite sheet for its icon set.
const iconID = getSpriteSheetName(idKey, setName);
const iconID = getSpriteSheetName(iconNameToLoad, setName);

// Return SVG markup with a reference to the icon ID within the sprite sheet.
return html`<svg
Expand All @@ -211,7 +151,7 @@ export const Template = ({
aria-label=${iconName}
role="img"
>
<title id=${idKey}>${idKey.replace(/([A-Z])/g, " $1").trim()}</title>
<title id=${iconNameToLoad}>${iconNameToLoad.replace(/([A-Z])/g, " $1").trim()}</title>
<use xlink:href="#${iconID}" href="#${iconID}" />
</svg>`;
};
Expand Down Expand Up @@ -304,7 +244,7 @@ export const WorkflowDefaultTemplate = (args, context) => html`
},
context,
[
"AlertCircle",
"AlertTriangle",
"Bell",
"Camera",
"Color",
Expand All @@ -315,7 +255,7 @@ export const WorkflowDefaultTemplate = (args, context) => html`
"Files",
"Hand",
"Lightbulb",
"Paragraph",
"InfoCircle",
]
)}
`;
Expand Down
24 changes: 24 additions & 0 deletions components/icon/stories/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,27 @@ export const getUiIconSizes = (uiIcons) => {
};

export const uiIconSizes = getUiIconSizes(uiIconsWithDirections);

/**
* If UI icon name does not have a sizing number appended, add one to approximate the provided
* t-shirt sizing for the component, based on the most common mapping.
*
* @param {string} uiIconName
* @param {string} size t-shirt sizing
* @returns {string} uiIconName with appended default sizing number, if one is not already present.
*/
export const appendUiIconDefaultSizing = (uiIconName, size = "m") => {
// If icon name already has a size number on the end, no change is needed.
if (uiIconName.match(/\d{2,3}$/)) {
return uiIconName;
}

return uiIconName + ({
xs: "50",
s: "75",
m: "100",
l: "200",
xl: "300",
xxl: "400",
}[size] || "100");
};
Loading