Skip to content

Commit

Permalink
feat: added chip component
Browse files Browse the repository at this point in the history
  • Loading branch information
Swappea committed Oct 27, 2024
1 parent 7631960 commit 99ba32e
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 0 deletions.
53 changes: 53 additions & 0 deletions apps/docs/src/examples/chip.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.section {
display: flex;
flex-direction: row;
column-gap: 10px;
}

.chip {
display: flex;
column-gap: 2px;
border: 1px solid black;
width: max-content;
padding: 5px 10px;
border-radius: 30px;
cursor: pointer;
background-color: #0369a0;
}

.chip:hover {
background-color: #0092e0;
}

.chip__label {
color: #fff;
}

.chip[aria-disabled="true"] {
background-color: #0369a0;
opacity: 0.5;
cursor: not-allowed;
}

.chip__deletable {
display: flex;
column-gap: 10px;
border: 1px solid black;
background-color: aliceblue;
width: max-content;
padding: 5px 10px;
border-radius: 30px;
cursor: pointer;
background-color: #0369a0;
}

.chip__deletable:hover {
background-color: #0092e0;
}


.delete {
color: black;
font-weight: 500;
font-size: 16px;
}
25 changes: 25 additions & 0 deletions apps/docs/src/examples/chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Chip } from "@kobalte/core/chip";

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

console.log('style', style);

export function BasicExample() {
return (
<div class={style.section}>
<Chip onClick={() => console.log('onClickHandler - Chip A')} class={style.chip}>
<Chip.Label class={style.chip__label}>Chip A</Chip.Label>
</Chip>
<Chip onClick={() => console.log('onClickHandler - Chip B')} class={style.chip}>
<Chip.Label class={style.chip__label}>Chip B</Chip.Label>
</Chip>
<Chip onClick={() => console.log('this is a disabled chip')} class={style.chip} disabled={true}>
<Chip.Label class={style.chip__label}>Disabled Chip C</Chip.Label>
</Chip>
<Chip onClick={() => console.log('delete me on click!!!')} class={style.chip__deletable}>
<Chip.Label class={style.chip__label}>Deletable Chip D</Chip.Label>
<span class={style.delete}>X</span>
</Chip>
</div>
)
}
4 changes: 4 additions & 0 deletions apps/docs/src/routes/docs/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ const CORE_NAV_SECTIONS: NavSection[] = [
title: "Checkbox",
href: "/docs/core/components/checkbox",
},
{
title: "Chip",
href: "/docs/core/components/chip",
},
{
title: "Collapsible",
href: "/docs/core/components/collapsible",
Expand Down
162 changes: 162 additions & 0 deletions apps/docs/src/routes/docs/core/components/chip.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Preview, TabsSnippets, Kbd } from "../../../../components";
import { BasicExample } from "../../../../examples/chip";

# Chip

- Chips are compact elements that represent an input or an action.

## Import

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

## Features

- **Flexible Usage**: Can be used as a simple display tag, an interactive button, or a deletable item.
- **Keyboard Accessible**: Supports `Enter` and `Space` key interactions to ensure accessibility when clickable.
- **Polymorphic Design**: By default, the chip is a `<div>` with `role="button"` for accessibility but can easily be wrapped in other elements if needed.
- **ARIA Attributes**: Implements necessary ARIA roles for accessibility (`role="button"`, `aria-disabled`).
- **Composable Subcomponents**: Includes a `Chip.Label` component to separate label content from other chip elements.

## Anatomy

The Chip consists of:

- **Chip** - The root container for the chip.
- **Chip.Label** - The label that is highly configurable which gives information to the user.

```tsx
<Chip>
<Chip.Label></Chip.Label>
</Chip>
```

## 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 { Chip } from "@kobalte/core/chip";
import "./style.css";

function App() {
return (
<div class={section}>
<Chip onClick={() => console.log('onClickHandler - Chip A')} class={chip}>
<Chip.Label class={chip__label}>Chip A</Chip.Label>
</Chip>
<Chip onClick={() => console.log('onClickHandler - Chip B')} class={chip}>
<Chip.Label class={chip__label}>Chip B</Chip.Label>
</Chip>
<Chip onClick={() => console.log('this is a disabled chip')} class={chip} disabled={true}>
<Chip.Label class={chip__label}>Disabled Chip C</Chip.Label>
</Chip>
<Chip onClick={() => console.log('delete me on click!!!')} class={chip__deletable}>
<Chip.Label class={chip__label}>Deletable Chip D</Chip.Label>
<span class={delete}>X</span>
</Chip>
</div>
);
}
```

</TabsSnippets.Content>
<TabsSnippets.Content value="style.css">
```css
.section {
display: flex;
flex-direction: row;
column-gap: 10px;
}

.chip {
display: flex;
column-gap: 2px;
border: 1px solid black;
width: max-content;
padding: 5px 10px;
border-radius: 30px;
cursor: pointer;
background-color: #0369a0;
}

.chip:hover {
background-color: #0092e0;
}

.chip__label {
color: #fff;
}

.chip[aria-disabled="true"] {
background-color: #0369a0;
opacity: 0.5;
cursor: not-allowed;
}

.chip__deletable {
display: flex;
column-gap: 10px;
border: 1px solid black;
background-color: aliceblue;
width: max-content;
padding: 5px 10px;
border-radius: 30px;
cursor: pointer;
background-color: #0369a0;
}

.chip__deletable:hover {
background-color: #0092e0;
}

.delete {
color: black;
font-weight: 500;
font-size: 16px;
}
```

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

## API Reference

### Chip

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

| Prop | Description |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| disabled | `boolean` <br/> If true, the component is disabled. |
| onClick | Callback function to handle onClick events. Keyboard events for enter and space would also trigger this callback on pressing `Enter` or `Space`. |

### Chip.Label

| Prop | Description |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` <br/> Uses label, if no children is provided |

## Accessibility

### Keyboard Interactions

| Key | Description |
| :--------------- | :------------------------------- |
| <Kbd>Enter</Kbd> | Triggers the onClick handler when pressed. |
| <Kbd>Space</Kbd> | Triggers the onClick handler when pressed. |
Empty file.
22 changes: 22 additions & 0 deletions packages/core/src/chip/chip-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type ValidComponent, type JSX, splitProps } from "solid-js";
import { type ElementOf, Polymorphic, type PolymorphicProps } from "../polymorphic";

export interface LabelOptions {
label?: string;
children?: JSX.Element;
}

export interface LabelCommonProps<T extends HTMLElement = HTMLElement> {
id?: string;
style?: JSX.CSSProperties | string;
}

export type LabelProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = LabelOptions & Partial<LabelCommonProps<ElementOf<T>>>;

export function Label<T extends ValidComponent = "span">(props: PolymorphicProps<T, LabelProps<T>>) {
const [local, others] = splitProps(props, ["label", "children"]);

return <Polymorphic as="span" class="chip__label" {...others}>{local.label || local.children}</Polymorphic>
}
61 changes: 61 additions & 0 deletions packages/core/src/chip/chip-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createSignal, mergeProps, splitProps, type JSX, type ValidComponent } from "solid-js";
import { type ElementOf, Polymorphic, type PolymorphicProps } from "../polymorphic";
import { mergeDefaultProps } from "@kobalte/utils";

export interface ChipRootOptions {
/** Event handler called when the chip is clicked. */
onClick?: () => void;
/** Whether to disable the chip or not... */
disabled?: boolean;
/** The children of the chip. */
children?: JSX.Element;
}

export interface ChipCommonProps<T extends HTMLElement = HTMLElement> {
id?: string;
style?: JSX.CSSProperties | string;
}

export type ChipRootProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = ChipRootOptions & Partial<ChipCommonProps<ElementOf<T>>>;

export function Chip<T extends ValidComponent = "div">(
props: PolymorphicProps<T, ChipRootProps<T>>,
) {
// Merging default values
const mergedProps = mergeDefaultProps(
{ disabled: false },
props as ChipRootProps,
);

const [local, others] = splitProps(mergedProps, ["disabled", "onClick"]);

const handleSelect = () => {
if (!local.disabled) {
local.onClick?.();
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (!local.disabled && (event.key === "Enter" || event.key === " ")) {
event.preventDefault(); // Prevents scrolling when using the Space key
handleSelect();
}
};

return (
<Polymorphic
as="div"
class="chip__root"
role="button"
tabindex={local.disabled ? -1 : 0}
aria-disabled={local.disabled}
onClick={handleSelect}
onKeyDown={handleKeyDown}
{...others}
>
{props.children}
</Polymorphic>
);
}
Loading

0 comments on commit 99ba32e

Please sign in to comment.