Skip to content

Commit

Permalink
feat: add Badge component
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiehenson committed Dec 5, 2024
1 parent b672423 commit bd53ad3
Show file tree
Hide file tree
Showing 3 changed files with 345 additions and 0 deletions.
153 changes: 153 additions & 0 deletions src/core/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, { PropsWithChildren, useMemo } from "react";
import { IconName, IconSize } from "./Icon/types";
import Icon from "./Icon";
import cn from "./utils/cn";
import { ColorClassColorGroups } from "./styles/colors/types";

/**
* Props for the Badge component.
*/
interface BadgeProps {
/**
* The size of the badge. Can be one of "xs", "sm", "md", or "lg".
*/
size?: "xs" | "sm" | "md" | "lg";

/**
* The color of the badge. Can be a value from ColorClassColorGroups or "red".
*/
color?: ColorClassColorGroups | "red";

/**
* The name of the icon to be displayed before the children in the badge.
*/
iconBefore?: IconName;

/**
* The name of the icon to be displayed after the children in the badge.
*/
iconAfter?: IconName;

/**
* Additional CSS class names to apply to the badge.
*/
className?: string;

/**
* Whether the badge is disabled. Defaults to false.
*/
disabled?: boolean;

/**
* Whether the badge is focusable. Defaults to false.
*/
focusable?: boolean;

/**
* Whether the badge is hoverable. Defaults to false.
*/
hoverable?: boolean;

/**
* The size of the icons in the badge. Defaults to 12px.
*/
iconSize?: IconSize;

/**
* Accessible label for the badge when interactive
*/
ariaLabel?: string;
}

const Badge: React.FC<PropsWithChildren<BadgeProps>> = ({
size = "md",
color = "neutral",
iconBefore,
iconAfter,
className,
children,
disabled = false,
focusable = false,
hoverable = false,
iconSize = "12px",
ariaLabel,
}) => {
const sizeClass = useMemo(() => {
switch (size) {
case "xs":
return "px-8 py-0 text-[10px] leading-tight";
case "sm":
return "px-8 py-2 text-[10px] leading-tight";
case "md":
return "px-[10px] py-2 text-[11px] leading-normal";
case "lg":
return "px-12 py-[3px] text-[12px] leading-normal";
}
}, [size]);

const childClass = useMemo(() => {
switch (size) {
case "xs":
case "sm":
return "leading-[18px]";
case "md":
case "lg":
return "leading-[20px]";
}
}, [size]);

const colorClass = useMemo(() => {
switch (color) {
case "neutral":
return "text-neutral-900 dark:text-neutral-400";
case "violet":
return "text-violet-400";
case "orange":
return "text-orange-600";
case "yellow":
return "text-yellow-600";
case "green":
return "text-green-600";
case "blue":
return "text-blue-600";
case "pink":
return "text-pink-600";
case "red":
return "text-orange-700";
}
}, [color]);

return (
<div
className={cn(
"inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold",
sizeClass,
colorClass,
{ "focus-base": focusable },
{
"hover:bg-neutral-300 hover:dark:bg-neutral-1000 active:bg-neutral-300 dark:active:bg-neutral-1000":
hoverable,
},
{
"cursor-not-allowed disabled:text-gui-unavailable dark:disabled:text-gui-unavailable-dark":
disabled,
},
className,
)}
tabIndex={focusable ? 0 : undefined}
aria-label={focusable || hoverable ? ariaLabel : undefined}
>
{iconBefore ? (
<Icon name={iconBefore} size={iconSize} color={colorClass} />
) : null}
<span className={cn("whitespace-nowrap tracking-widen-0.04", childClass)}>
{children}
</span>
{iconAfter ? (
<Icon name={iconAfter} size={iconSize} color={colorClass} />
) : null}
</div>
);
};

export default Badge;
109 changes: 109 additions & 0 deletions src/core/Badge/Badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from "react";
import { Meta, StoryFn } from "@storybook/react";
import Badge from "../Badge";

export default {
title: "Components/Badge",
component: Badge,
} as Meta;

const Template: StoryFn = (args) => <Badge {...args} />;

export const Default = Template.bind({});
Default.args = {
children: "BADGE",
};
Default.parameters = {
docs: {
description: {
story:
"The default badge component. Can be customized with various props. Use the `children` prop to set the text.",
},
},
};

export const WithBeforeIcon = Template.bind({});
WithBeforeIcon.args = {
children: "BADGE",
iconBefore: "icon-gui-check-lotus-circled",
};
WithBeforeIcon.parameters = {
docs: {
description: {
story:
"Badge component with an icon displayed before the text. Use the `iconBefore` prop to set the icon.",
},
},
};

export const WithAfterIcon = Template.bind({});
WithAfterIcon.args = {
children: "BADGE",
iconAfter: "icon-gui-chevron-down",
};
WithAfterIcon.parameters = {
docs: {
description: {
story:
"Badge component with an icon displayed after the text. Use the `iconAfter` prop to set the icon.",
},
},
};

export const Hoverable = Template.bind({});
Hoverable.args = {
children: "BADGE",
hoverable: true,
};
Hoverable.parameters = {
docs: {
description: {
story:
"Badge component that changes appearance when hovered over. Use the `hoverable` prop to enable this feature.",
},
},
};

export const Focusable = Template.bind({});
Focusable.args = {
children: "BADGE",
focusable: true,
};
Focusable.parameters = {
docs: {
description: {
story:
"Badge component that can be focused using keyboard navigation. Use the `focusable` prop to enable this feature.",
},
},
};

export const WithLargeIconSize = Template.bind({});
WithLargeIconSize.args = {
children: "BADGE",
iconBefore: "icon-gui-check-circled",
iconSize: "16px",
};
WithLargeIconSize.parameters = {
docs: {
description: {
story:
"Badge component with a larger icon size. Use the `iconSize` prop to set the size of the icon.",
},
},
};

export const Disabled = Template.bind({});
Disabled.args = {
children: "BADGE",
iconBefore: "icon-gui-check-circled",
disabled: true,
};
Disabled.parameters = {
docs: {
description: {
story:
"Badge component that is disabled and not interactive. Use the `disabled` prop to disable the badge.",
},
},
};
83 changes: 83 additions & 0 deletions src/core/Badge/__snapshots__/Badge.stories.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Components/Badge Default smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400">
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;

exports[`Components/Badge Disabled smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400 cursor-not-allowed disabled:text-gui-unavailable dark:disabled:text-gui-unavailable-dark">
<svg class="text-neutral-900 dark:text-neutral-400"
style="width: 12px; height: 12px;"
>
<use xlink:href="#sprite-icon-gui-check-circled">
</use>
</svg>
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;
exports[`Components/Badge Focusable smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400 focus-base"
tabindex="0"
>
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;
exports[`Components/Badge Hoverable smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400 hover:bg-neutral-300 hover:dark:bg-neutral-1000 active:bg-neutral-300 dark:active:bg-neutral-1000">
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;
exports[`Components/Badge WithAfterIcon smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400">
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
<svg class="text-neutral-900 dark:text-neutral-400"
style="width: 12px; height: 12px;"
>
<use xlink:href="#sprite-icon-gui-chevron-down">
</use>
</svg>
</div>
`;
exports[`Components/Badge WithBeforeIcon smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400">
<svg class="text-neutral-900 dark:text-neutral-400"
style="width: 12px; height: 12px;"
>
<use xlink:href="#sprite-icon-gui-check-lotus-circled">
</use>
</svg>
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;
exports[`Components/Badge WithLargeIconSize smoke-test 1`] = `
<div class="inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-4 items-center focus-base transition-colors select-none font-semibold px-[10px] py-2 text-[11px] leading-normal text-neutral-900 dark:text-neutral-400">
<svg class="text-neutral-900 dark:text-neutral-400"
style="width: 16px; height: 16px;"
>
<use xlink:href="#sprite-icon-gui-check-circled">
</use>
</svg>
<span class="whitespace-nowrap tracking-widen-0.04 leading-[20px]">
BADGE
</span>
</div>
`;

0 comments on commit bd53ad3

Please sign in to comment.