Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Provide a mechanism to swap out built-in component icons to custom icons #4002

Merged
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
11 changes: 11 additions & 0 deletions .changeset/grumpy-lies-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@salt-ds/core": minor
---

Added SemanticIconProvider to provide a mechanism to swap out built-in component icons to custom icons.

```tsx
<SemanticIconProvider iconMap={iconMap}>
<App />
</SemanticIconProvider>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { SemanticIconProvider, useIcon } from "@salt-ds/core";
import {
CalendarIcon,
ChevronDownIcon,
ChevronUpIcon,
DoubleChevronDownIcon,
DoubleChevronUpIcon,
SuccessTickIcon,
UserSolidIcon,
} from "@salt-ds/icons";

const TestComponent = () => {
const icons = useIcon();
return (
<div>
<icons.ExpandIcon data-testid="ChevronDownIcon" />
<icons.CollapseIcon data-testid="ChevronUpIcon" />
<icons.SuccessIcon data-testid="SuccessTickIcon" />
<icons.CalendarIcon data-testid="CalendarIcon" />
<icons.UserIcon data-testid="UserSolidIcon" />
</div>
);
};

describe("SemanticIconProvider", () => {
it("should use default icons when provider is not wrapped", () => {
cy.mount(<TestComponent />);
cy.get('[data-testid="ChevronDownIcon"]').should("exist");
cy.get('[data-testid="SuccessTickIcon"]').should("exist");
cy.get('[data-testid="CalendarIcon"]').should("exist");
cy.get('[data-testid="UserSolidIcon"]').should("exist");
});

it("should support overriding only specific icons", () => {
cy.mount(
<SemanticIconProvider
iconMap={{
CollapseIcon: DoubleChevronUpIcon,
ExpandIcon: DoubleChevronDownIcon,
}}
>
<TestComponent />
</SemanticIconProvider>,
);

cy.get('[data-testid="ChevronDownIcon"]').should(
"have.attr",
"aria-label",
"double chevron down",
);
cy.get('[data-testid="ChevronUpIcon"]').should(
"have.attr",
"aria-label",
"double chevron up",
);

cy.get('[data-testid="SuccessTickIcon"]').should("exist");
cy.get('[data-testid="CalendarIcon"]').should("exist");
cy.get('[data-testid="UserSolidIcon"]').should("exist");
});

it("should support overriding all icons", () => {
cy.mount(
<SemanticIconProvider
iconMap={{
CollapseIcon: ChevronDownIcon,
ExpandIcon: ChevronUpIcon,
SuccessIcon: UserSolidIcon,
CalendarIcon: SuccessTickIcon,
UserIcon: CalendarIcon,
}}
>
<TestComponent />
</SemanticIconProvider>,
);
cy.get('[data-testid="ChevronDownIcon"]').should(
"have.attr",
"aria-label",
"chevron up",
);
cy.get('[data-testid="ChevronUpIcon"]').should(
"have.attr",
"aria-label",
"chevron down",
);
cy.get('[data-testid="SuccessTickIcon"]').should(
"have.attr",
"aria-label",
"user solid",
);
cy.get('[data-testid="CalendarIcon"]').should(
"have.attr",
"aria-label",
"success tick",
);
cy.get('[data-testid="UserSolidIcon"]').should(
"have.attr",
"aria-label",
"calendar",
);
});
});
8 changes: 4 additions & 4 deletions packages/core/src/accordion/AccordionHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
Expand All @@ -8,8 +7,8 @@ import {
type ReactNode,
forwardRef,
} from "react";
import { useIcon } from "../semantic-icon-provider";
import { StatusIndicator } from "../status-indicator";

import { makePrefixer, useIsomorphicLayoutEffect } from "../utils";

import { useAccordion } from "./AccordionContext";
Expand All @@ -26,11 +25,12 @@ export interface AccordionHeaderProps
const withBaseName = makePrefixer("saltAccordionHeader");

function ExpansionIcon({ expanded }: { expanded: boolean }) {
const { CollapseIcon, ExpandIcon } = useIcon();
if (expanded) {
return <ChevronUpIcon aria-hidden className={withBaseName("icon")} />;
return <CollapseIcon aria-hidden className={withBaseName("icon")} />;
}

return <ChevronDownIcon aria-hidden className={withBaseName("icon")} />;
return <ExpandIcon aria-hidden className={withBaseName("icon")} />;
}

export const AccordionHeader = forwardRef<
Expand Down
16 changes: 12 additions & 4 deletions packages/core/src/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { UserSolidIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
import { type HTMLAttributes, type ReactNode, forwardRef } from "react";
import { useAvatarImage } from "./useAvatarImage";

import { useIcon } from "../semantic-icon-provider";
import { makePrefixer } from "../utils";
import avatarCss from "./Avatar.css";
import { useAvatarImage } from "./useAvatarImage";

export type NameToInitials = (name?: string) => string;

Expand Down Expand Up @@ -54,12 +53,21 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
src,
size = DEFAULT_AVATAR_SIZE,
style: styleProp,
fallbackIcon = <UserSolidIcon aria-label="User Avatar" />,
fallbackIcon: fallbackIconProp,
...rest
},
ref,
) {
const targetWindow = useWindow();
const { UserIcon } = useIcon();

const fallbackIcon =
fallbackIconProp === undefined ? (
<UserIcon aria-label="User Avatar" />
) : (
fallbackIconProp
);

useComponentCssInjection({
testId: "salt-avatar",
css: avatarCss,
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/combo-box/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
useFocus,
useInteractions,
} from "@floating-ui/react";
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
Expand All @@ -34,6 +33,7 @@ import {
import { defaultValueToString } from "../list-control/ListControlState";
import { OptionList } from "../option/OptionList";
import { PillInput, type PillInputProps } from "../pill-input";
import { useIcon } from "../semantic-icon-provider";
import {
type UseFloatingUIProps,
makePrefixer,
Expand Down Expand Up @@ -97,7 +97,7 @@ export const ComboBox = forwardRef(function ComboBox<Item>(
css: comboBoxCss,
window: targetWindow,
});

const { CollapseIcon, ExpandIcon } = useIcon();
const {
a11yProps: { "aria-labelledby": formFieldLabelledBy } = {},
disabled: formFieldDisabled,
Expand Down Expand Up @@ -429,9 +429,9 @@ export const ComboBox = forwardRef(function ComboBox<Item>(
tabIndex={-1}
>
{openState ? (
<ChevronUpIcon aria-hidden />
<CollapseIcon aria-hidden />
) : (
<ChevronDownIcon aria-hidden />
<ExpandIcon aria-hidden />
)}
</Button>
) : undefined}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/dialog/DialogCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CloseIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import clsx from "clsx";
import { forwardRef } from "react";
import { Button, type ButtonProps } from "../button";
import { useIcon } from "../semantic-icon-provider";
import { makePrefixer } from "../utils";

import dialogCloseButtonCss from "./DialogCloseButton.css";
Expand All @@ -18,6 +18,7 @@ export const DialogCloseButton = forwardRef<HTMLButtonElement, ButtonProps>(
css: dialogCloseButtonCss,
window: targetWindow,
});
const { CloseIcon } = useIcon();

return (
<Button
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/drawer/DrawerCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CloseIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import clsx from "clsx";
import { forwardRef } from "react";
import { Button, type ButtonProps } from "../button";
import { useIcon } from "../semantic-icon-provider";
import { makePrefixer } from "../utils";

import drawerCloseButtonCss from "./DrawerCloseButton.css";
Expand All @@ -18,6 +18,7 @@ export const DrawerCloseButton = forwardRef<HTMLButtonElement, ButtonProps>(
css: drawerCloseButtonCss,
window: targetWindow,
});
const { CloseIcon } = useIcon();

return (
<div className={withBaseName("container")}>
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
useFocus,
useInteractions,
} from "@floating-ui/react";
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
Expand All @@ -34,6 +33,7 @@ import {
useListControl,
} from "../list-control/ListControlState";
import { OptionList } from "../option/OptionList";
import { useIcon } from "../semantic-icon-provider";
import { StatusAdornment } from "../status-adornment";
import type { ValidationStatus } from "../status-indicator";
import {
Expand Down Expand Up @@ -98,7 +98,8 @@ export type DropdownProps<Item = string> = {
ListControlProps<Item>;

function ExpandIcon({ open }: { open: boolean }) {
return open ? <ChevronUpIcon aria-hidden /> : <ChevronDownIcon aria-hidden />;
const { CollapseIcon, ExpandIcon } = useIcon();
return open ? <CollapseIcon aria-hidden /> : <ExpandIcon aria-hidden />;
}

const withBaseName = makePrefixer("saltDropdown");
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/file-drop-zone/FileDropZoneIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type IconProps, UploadIcon } from "@salt-ds/icons";
import { forwardRef } from "react";
import { useIcon } from "../semantic-icon-provider";
import { StatusIndicator, type ValidationStatus } from "../status-indicator";

export interface FileDropZoneIconProps extends IconProps {
Expand All @@ -18,6 +19,8 @@ export const FileDropZoneIcon = forwardRef<
size,
...rest,
};

const { UploadIcon } = useIcon();
return status ? (
<StatusIndicator status={status} {...iconProps} />
) : (
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ export * from "./tooltip";
export * from "./utils";
export * from "./viewport";
export * from "./types";
export * from "./semantic-icon-provider";
18 changes: 11 additions & 7 deletions packages/core/src/link/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { type IconProps, TearOutIcon } from "@salt-ds/icons";
import type { IconProps } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
import { type ComponentType, type ReactElement, forwardRef } from "react";
import { useIcon } from "../semantic-icon-provider";
import { Text, type TextProps } from "../text";
import { makePrefixer } from "../utils";

import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import linkCss from "./Link.css";

const withBaseName = makePrefixer("saltLink");
Expand All @@ -22,7 +22,7 @@ export interface LinkProps extends Omit<TextProps<"a">, "as" | "disabled"> {

export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
{
IconComponent = TearOutIcon,
IconComponent,
href,
className,
children,
Expand All @@ -39,6 +39,10 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
css: linkCss,
window: targetWindow,
});
const { ExternalIcon } = useIcon();

const LinkIconComponent =
IconComponent === undefined ? ExternalIcon : IconComponent;

return (
<Text
Expand All @@ -54,8 +58,8 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
{children}
{target === "_blank" && (
<>
{IconComponent && (
<IconComponent className={withBaseName("icon")} aria-hidden />
{LinkIconComponent && (
<LinkIconComponent className={withBaseName("icon")} aria-hidden />
)}
<span className={withBaseName("externalLinkADA")}>External</span>
</>
Expand Down
9 changes: 3 additions & 6 deletions packages/core/src/menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useFloatingTree, useListItem } from "@floating-ui/react";
import { ChevronRightIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
Expand All @@ -12,10 +11,10 @@ import {
} from "react";
import { makePrefixer, useForkRef } from "../utils";

import { useIcon } from "../semantic-icon-provider";
import menuItemCss from "./MenuItem.css";
import { useMenuPanelContext } from "./MenuPanelContext";
import { useIsMenuTrigger } from "./MenuTriggerContext";

export interface MenuItemProps extends ComponentPropsWithoutRef<"div"> {
/**
* If `true`, the item will be disabled.
Expand All @@ -38,6 +37,7 @@ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
} = props;

const { triggersSubmenu, blurActive } = useIsMenuTrigger();
const { ExpandGroupIcon } = useIcon();
const { activeIndex, getItemProps, setFocusInside } = useMenuPanelContext();
const item = useListItem();
const tree = useFloatingTree();
Expand Down Expand Up @@ -96,10 +96,7 @@ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
>
{children}
{triggersSubmenu && (
<ChevronRightIcon
className={withBaseName("expandIcon")}
aria-hidden
/>
<ExpandGroupIcon className={withBaseName("expandIcon")} aria-hidden />
)}
</div>
);
Expand Down
Loading
Loading