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

feat: Badge component #503

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions apps/docs/src/examples/badge.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.badge {
display: inline-block;
padding: 0.2em 0.4em;
background-color: hsl(201 96% 32%);
color: white; border-radius: 12px;
font-size: 0.875rem;
}
11 changes: 11 additions & 0 deletions apps/docs/src/examples/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Badge } from "@kobalte/core/badge";

import style from "./badge.module.css";

export function BasicExample() {
return (
<Badge class={style.badge} textValue="Unread messages: 5">
5 messages
</Badge>
);
}
5 changes: 5 additions & 0 deletions apps/docs/src/routes/docs/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
title: "Alert Dialog",
href: "/docs/core/components/alert-dialog",
},
{
title: "Badge",
href: "/docs/core/components/badge",
status: "new",
},
{
title: "Breadcrumbs",
href: "/docs/core/components/breadcrumbs",
Expand Down
94 changes: 94 additions & 0 deletions apps/docs/src/routes/docs/core/components/badge.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Preview, TabsSnippets, Callout } from "../../../../components";
import { BasicExample } from "../../../../examples/badge";

# Badge

A `Badge` component is used to display small pieces of information or status indicators.

## Import

```ts
import { Badge } from "@kobalte/core/badge";
// or
import { Root } from "@kobalte/core/badge";
// or (deprecated)
import { Badge } from "@kobalte/core";
```

## Features

- Auto-populated ARIA labeling via the textValue prop for enhanced accessibility.
- Built-in ARIA support with role="status" to communicate dynamic updates.

## Anatomy

The badge consists of:

- **Badge:** The root container for the badge that supports accessibility and content customization.

```tsx
<Badge textValue="3 online users">3</Badge>
```

## Example

<Preview>
<BasicExample />
</Preview>

<TabsSnippets>
<TabsSnippets.List>
<TabsSnippets.Trigger value="index.tsx">index.tsx</TabsSnippets.Trigger>
<TabsSnippets.Trigger value="style.css">style.css</TabsSnippets.Trigger>
</TabsSnippets.List>
{/* <!-- prettier-ignore-start -->*/}
<TabsSnippets.Content value="index.tsx">
```tsx
import { Badge } from "@kobalte/core/badge";
import "./style.css";

function App() {
return (
<Badge class="badge" textValue="Unread messages: 5">
5 messages
</Badge>
);
}
```

</TabsSnippets.Content>
<TabsSnippets.Content value="style.css">
```css
.badge {
display: inline-block;
padding: 0.2em 0.4em;
background-color: hsl(201 96% 32%);
color: white; border-radius: 12px;
font-size: 0.875rem;
}
```

</TabsSnippets.Content>
{/* <!-- prettier-ignore-end -->*/}
</TabsSnippets>


## API Reference

### Badge

`Badge` is equivalent to the `Root` import from `@kobalte/core/badge` (and deprecated `Badge.Root`).

| Prop | Description |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| textValue | `string \| undefined` <br/> The textValue prop is essential for accessibility. It auto-populates the aria-label attribute, allowing screen readers to interpret the badge’s content. |

| Data attribute | Description |
| :------------- | :------------------------------------------------------------ |
| data-label | Present when textValue is passed or aria-label is passed as a prop. |

## Rendered elements

| Component | Default rendered element |
| :---------------- | :------------------------ |
| `Badge` | `span` |
7 changes: 6 additions & 1 deletion packages/core/dev/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Badge } from "../src/badge";

export default function App() {
return (
<></>
<>
<Badge as="div" class="aaall" aria-label="qqqq" aria-checked textValue="aaaa">aaa1123</Badge>
<Badge textValue="10">11</Badge>
</>
);
}
46 changes: 46 additions & 0 deletions packages/core/src/badge/badge-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Portions of this file are based on code from react-spectrum.
* Apache License Version 2.0, Copyright 2020 Adobe.
*
* Credits to the React Spectrum team:
* https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/badge/src/Badge.tsx
*/

import { type ValidComponent, splitProps } from "solid-js";
import { Polymorphic, type PolymorphicProps } from "../polymorphic";

export interface BadgeRootOptions {
/**
* Optional textValue for badge.
*/
textValue?: string;
}

export interface BadgeRootCommonProps<T extends HTMLElement = HTMLElement> {
"aria-label"?: string;
}

export interface BadgeRootRenderProps extends BadgeRootCommonProps {}

export type BadgeRootProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = BadgeRootOptions & Partial<BadgeRootCommonProps>;

export function BadgeRoot<T extends ValidComponent = "span">(
props: PolymorphicProps<T, BadgeRootProps<T>>,
) {
const [local, others] = splitProps(props, ["textValue", "aria-label"]);

const ariaLabel = () => local["aria-label"] || local.textValue;

return (
<Polymorphic<BadgeRootRenderProps>
as="span"
role="status"
aria-label={ariaLabel()}
{...others}
>
{others.children}
</Polymorphic>
);
}
33 changes: 33 additions & 0 deletions packages/core/src/badge/badge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { render } from "@solidjs/testing-library";
import * as Badge from ".";

describe("Badge", () => {
it("badge with textValue", () => {
const { getByRole } = render(() => (
<Badge.Root textValue="Online">Online</Badge.Root>
));

const badge = getByRole("status");
expect(badge).toHaveTextContent("Online");
});

it("badge with defined aria-label", () => {
const { getByText } = render(() => (
<Badge.Root textValue="Online" aria-label="User is online">
Online
</Badge.Root>
));

const badge = getByText("Online");
expect(badge).toHaveAttribute("aria-label", "User is online");
});

it("badge with default aria-labe as textValue", () => {
Shubhdeep12 marked this conversation as resolved.
Show resolved Hide resolved
const { getByRole } = render(() => (
<Badge.Root textValue="Online">Online</Badge.Root>
));

const badge = getByRole("status");
expect(badge).toHaveAttribute("aria-label", "Online");
});
});
17 changes: 17 additions & 0 deletions packages/core/src/badge/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
type BadgeRootCommonProps,
type BadgeRootOptions,
type BadgeRootProps,
type BadgeRootRenderProps,
BadgeRoot as Root,
} from "./badge-root";

export type {
BadgeRootOptions,
BadgeRootCommonProps,
BadgeRootRenderProps,
BadgeRootProps,
};
export { Root };

export const Badge = Root;
1 change: 1 addition & 0 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./toast/toaster";
export * as Accordion from "./accordion";
export * as Alert from "./alert";
export * as AlertDialog from "./alert-dialog";
export * as Badge from "./badge";
export * as Breadcrumbs from "./breadcrumbs";
export * as Button from "./button";
//export * as Calendar from "./calendar";
Expand Down