From 99ba32e66dbbeb5ecbfa14e1670da2ef6fe8c2ab Mon Sep 17 00:00:00 2001 From: Swapnesh Date: Mon, 28 Oct 2024 02:34:48 +0530 Subject: [PATCH] feat: added chip component --- apps/docs/src/examples/chip.module.css | 53 ++++++ apps/docs/src/examples/chip.tsx | 25 +++ apps/docs/src/routes/docs/core.tsx | 4 + .../src/routes/docs/core/components/chip.mdx | 162 ++++++++++++++++++ packages/core/src/chip/chip-delete.tsx | 0 packages/core/src/chip/chip-label.tsx | 22 +++ packages/core/src/chip/chip-root.tsx | 61 +++++++ packages/core/src/chip/chip.test.tsx | 68 ++++++++ packages/core/src/chip/index.tsx | 30 ++++ packages/core/src/index.tsx | 1 + 10 files changed, 426 insertions(+) create mode 100644 apps/docs/src/examples/chip.module.css create mode 100644 apps/docs/src/examples/chip.tsx create mode 100644 apps/docs/src/routes/docs/core/components/chip.mdx create mode 100644 packages/core/src/chip/chip-delete.tsx create mode 100644 packages/core/src/chip/chip-label.tsx create mode 100644 packages/core/src/chip/chip-root.tsx create mode 100644 packages/core/src/chip/chip.test.tsx create mode 100644 packages/core/src/chip/index.tsx diff --git a/apps/docs/src/examples/chip.module.css b/apps/docs/src/examples/chip.module.css new file mode 100644 index 000000000..403f423bc --- /dev/null +++ b/apps/docs/src/examples/chip.module.css @@ -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; +} diff --git a/apps/docs/src/examples/chip.tsx b/apps/docs/src/examples/chip.tsx new file mode 100644 index 000000000..d141496fb --- /dev/null +++ b/apps/docs/src/examples/chip.tsx @@ -0,0 +1,25 @@ +import { Chip } from "@kobalte/core/chip"; + +import style from "./chip.module.css"; + +console.log('style', style); + +export function BasicExample() { + return ( +
+ console.log('onClickHandler - Chip A')} class={style.chip}> + Chip A + + console.log('onClickHandler - Chip B')} class={style.chip}> + Chip B + + console.log('this is a disabled chip')} class={style.chip} disabled={true}> + Disabled Chip C + + console.log('delete me on click!!!')} class={style.chip__deletable}> + Deletable Chip D + X + +
+ ) +} diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx index d4b1efb21..d418ec616 100644 --- a/apps/docs/src/routes/docs/core.tsx +++ b/apps/docs/src/routes/docs/core.tsx @@ -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", diff --git a/apps/docs/src/routes/docs/core/components/chip.mdx b/apps/docs/src/routes/docs/core/components/chip.mdx new file mode 100644 index 000000000..e6652c853 --- /dev/null +++ b/apps/docs/src/routes/docs/core/components/chip.mdx @@ -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 `
` 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 + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx +import { Chip } from "@kobalte/core/chip"; +import "./style.css"; + +function App() { + return ( +
+ console.log('onClickHandler - Chip A')} class={chip}> + Chip A + + console.log('onClickHandler - Chip B')} class={chip}> + Chip B + + console.log('this is a disabled chip')} class={chip} disabled={true}> + Disabled Chip C + + console.log('delete me on click!!!')} class={chip__deletable}> + Deletable Chip D + X + +
+ ); + } + ``` + +
+ + ```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; + } + ``` + + + {/* */} +
+ +## API Reference + +### Chip + +`Chip` is equivalent to the `Root` import from `@kobalte/core/chip` (and deprecated `Chip.Root`). + +| Prop | Description | +| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| disabled | `boolean`
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`
Uses label, if no children is provided | + +## Accessibility + +### Keyboard Interactions + +| Key | Description | +| :--------------- | :------------------------------- | +| Enter | Triggers the onClick handler when pressed. | +| Space | Triggers the onClick handler when pressed. | diff --git a/packages/core/src/chip/chip-delete.tsx b/packages/core/src/chip/chip-delete.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/chip/chip-label.tsx b/packages/core/src/chip/chip-label.tsx new file mode 100644 index 000000000..9050b631d --- /dev/null +++ b/packages/core/src/chip/chip-label.tsx @@ -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 { + id?: string; + style?: JSX.CSSProperties | string; +} + +export type LabelProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = LabelOptions & Partial>>; + +export function Label(props: PolymorphicProps>) { + const [local, others] = splitProps(props, ["label", "children"]); + + return {local.label || local.children} +} diff --git a/packages/core/src/chip/chip-root.tsx b/packages/core/src/chip/chip-root.tsx new file mode 100644 index 000000000..0c38a8eda --- /dev/null +++ b/packages/core/src/chip/chip-root.tsx @@ -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 { + id?: string; + style?: JSX.CSSProperties | string; +} + +export type ChipRootProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = ChipRootOptions & Partial>>; + +export function Chip( + props: PolymorphicProps>, +) { + // 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 ( + + {props.children} + + ); +} diff --git a/packages/core/src/chip/chip.test.tsx b/packages/core/src/chip/chip.test.tsx new file mode 100644 index 000000000..27a35cf70 --- /dev/null +++ b/packages/core/src/chip/chip.test.tsx @@ -0,0 +1,68 @@ +import { fireEvent, render } from "@solidjs/testing-library"; +import { afterAll, describe, expect, it, vi } from "vitest"; + +import * as Chip from "."; + +describe("Chip", () => { + const consoleMock = vi + .spyOn(console, "log") + .mockImplementation(() => undefined); + + afterEach(() => { + consoleMock.mockClear(); + }); + + afterAll(() => { + consoleMock.mockReset(); + }); + + it("should show Chip and it should be clickable", () => { + const { getByRole } = render(() => ( + console.log("hello world from chip A")}> + Chip A + + )); + + const button = getByRole("button", { name: /chip a/i }); + fireEvent.click(button); + expect(consoleMock).toHaveBeenLastCalledWith("hello world from chip A"); + }); + + it("should show disabled Chip and it should not be clickable", () => { + const { getByRole } = render(() => ( + console.log("not called")} disabled> + Disabled Chip A + + )); + + const button = getByRole("button", { name: /disabled chip a/i }); + fireEvent.click(button); + expect(consoleMock).not.toHaveBeenCalled(); + }); + + describe("keyboard accessible", () => { + it("should show Chip and it should be clickable via keyboard enter", () => { + const { getByRole } = render(() => ( + console.log("hello world from chip A")}> + Chip A + + )); + + const button = getByRole("button", { name: /chip a/i }); + fireEvent.keyDown(button, { key: "Enter", code: "Enter", charCode: 13 }); + expect(consoleMock).toHaveBeenCalledWith("hello world from chip A"); + }); + + it("should show Chip and it should be clickable via keyboard space", () => { + const { getByRole } = render(() => ( + console.log("hello world from chip B")}> + Chip B + + )); + + const button = getByRole("button", { name: /chip b/i }); + fireEvent.keyDown(button, { key: " ", code: "Space", charCode: 32 }); + expect(consoleMock).toHaveBeenCalledWith("hello world from chip B"); + }); + }); +}); diff --git a/packages/core/src/chip/index.tsx b/packages/core/src/chip/index.tsx new file mode 100644 index 000000000..5811f7b9a --- /dev/null +++ b/packages/core/src/chip/index.tsx @@ -0,0 +1,30 @@ + + +import { + Label, + type LabelCommonProps, + type LabelOptions, + type LabelProps, +} from "./chip-label"; + +import { + type ChipCommonProps, + type ChipRootOptions, + type ChipRootProps, + Chip as Root, +} from "./chip-root"; + +export type { + LabelCommonProps, + LabelOptions, + LabelProps, + ChipCommonProps, + ChipRootOptions, + ChipRootProps, +}; + +export { Root, Label }; + +export const Chip = Object.assign(Root, { + Label, +}); diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 4a72a633e..b1fd520b5 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -16,6 +16,7 @@ export * as AlertDialog from "./alert-dialog"; export * as Breadcrumbs from "./breadcrumbs"; export * as Button from "./button"; //export * as Calendar from "./calendar"; +export * as Chip from "./chip"; export * as Checkbox from "./checkbox"; export * as Collapsible from "./collapsible"; export * as Combobox from "./combobox";