From 82f8eccb176a9377a7af07c1a6057366698a7d28 Mon Sep 17 00:00:00 2001
From: lisalupi <106706307+lisalupi@users.noreply.github.com>
Date: Thu, 24 Oct 2024 16:21:55 +0200
Subject: [PATCH] feat(Chip): new component (#4307)
* feat: new chip component
* fix: feedback
* fix: feedback
* fix: text tag
---
.changeset/sharp-shoes-report.md | 5 +
.../ui/src/components/Chip/ChipContext.tsx | 9 +
packages/ui/src/components/Chip/ChipIcon.tsx | 81 ++
.../Chip/__stories__/Disabled.stories.tsx | 24 +
.../Chip/__stories__/Groups.stories.tsx.tsx | 122 +++
.../Chip/__stories__/Icons.stories.tsx | 24 +
.../Chip/__stories__/Playground.stories.tsx | 3 +
.../Chip/__stories__/Size.stories.tsx | 14 +
.../Chip/__stories__/Template.stories.tsx | 6 +
.../Chip/__stories__/index.stories.tsx | 23 +
.../__snapshots__/index.test.tsx.snap | 970 ++++++++++++++++++
.../components/Chip/__tests__/index.test.tsx | 94 ++
packages/ui/src/components/Chip/index.tsx | 152 +++
packages/ui/src/components/index.ts | 1 +
14 files changed, 1528 insertions(+)
create mode 100644 .changeset/sharp-shoes-report.md
create mode 100644 packages/ui/src/components/Chip/ChipContext.tsx
create mode 100644 packages/ui/src/components/Chip/ChipIcon.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Disabled.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Groups.stories.tsx.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Icons.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Playground.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Size.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/Template.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__stories__/index.stories.tsx
create mode 100644 packages/ui/src/components/Chip/__tests__/__snapshots__/index.test.tsx.snap
create mode 100644 packages/ui/src/components/Chip/__tests__/index.test.tsx
create mode 100644 packages/ui/src/components/Chip/index.tsx
diff --git a/.changeset/sharp-shoes-report.md b/.changeset/sharp-shoes-report.md
new file mode 100644
index 0000000000..cdb8289772
--- /dev/null
+++ b/.changeset/sharp-shoes-report.md
@@ -0,0 +1,5 @@
+---
+"@ultraviolet/ui": minor
+---
+
+New component ``
diff --git a/packages/ui/src/components/Chip/ChipContext.tsx b/packages/ui/src/components/Chip/ChipContext.tsx
new file mode 100644
index 0000000000..a7d726090a
--- /dev/null
+++ b/packages/ui/src/components/Chip/ChipContext.tsx
@@ -0,0 +1,9 @@
+import type { RefObject } from 'react'
+import { createContext } from 'react'
+
+type ContextType = {
+ isActive: boolean
+ disabled: boolean
+ iconRef?: RefObject
+}
+export const ChipContext = createContext(undefined)
diff --git a/packages/ui/src/components/Chip/ChipIcon.tsx b/packages/ui/src/components/Chip/ChipIcon.tsx
new file mode 100644
index 0000000000..d3b520bdb8
--- /dev/null
+++ b/packages/ui/src/components/Chip/ChipIcon.tsx
@@ -0,0 +1,81 @@
+import styled from '@emotion/styled'
+import * as Icon from '@ultraviolet/icons'
+import { useContext } from 'react'
+import type { PascalToCamelCaseWithoutSuffix } from 'src/types'
+import { ChipContext } from './ChipContext'
+
+const Container = styled.button`
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: ${({ theme }) => theme.radii.default};
+
+ &[data-has-onclick="true"][data-active="false"]:hover{
+ background-color: ${({ theme }) => theme.colors.neutral.backgroundStrongHover};
+ }
+
+ &[data-has-onclick="true"][data-active="true"]:hover{
+ background-color: ${({ theme }) => theme.colors.primary.backgroundStrong};
+ }
+
+ &[data-disabled="true"]{
+ cursor: not-allowed;
+ }
+`
+
+type IconType = PascalToCamelCaseWithoutSuffix
+
+type ChipIconType = {
+ /**
+ * Add an icon in the chip
+ */
+ name: IconType
+ onClick?: () => void
+ 'data-testid'?: string
+}
+
+export const ChipIcon = ({
+ name,
+ onClick,
+ 'data-testid': dataTestId,
+}: ChipIconType) => {
+ const context = useContext(ChipContext)
+
+ if (!context) {
+ throw new Error('Chip.Icon can only be used inside a Chip component')
+ }
+
+ const { disabled, isActive, iconRef } = context
+
+ const IconUsed =
+ Icon[
+ `${
+ (name as string).charAt(0).toUpperCase() + (name as string).slice(1)
+ }Icon` as keyof typeof Icon
+ ]
+
+ return (
+ {
+ if (!disabled && onClick) {
+ event.stopPropagation()
+ onClick()
+ }
+ }}
+ data-testid={dataTestId}
+ data-disabled={disabled}
+ data-active={isActive}
+ data-has-onclick={!!onClick && !disabled}
+ as={onClick ? 'button' : 'div'}
+ ref={iconRef}
+ >
+
+
+ )
+}
diff --git a/packages/ui/src/components/Chip/__stories__/Disabled.stories.tsx b/packages/ui/src/components/Chip/__stories__/Disabled.stories.tsx
new file mode 100644
index 0000000000..0bf269ec93
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Disabled.stories.tsx
@@ -0,0 +1,24 @@
+import type { StoryFn } from '@storybook/react'
+import { Chip } from '..'
+import { Stack } from '../../Stack'
+
+export const Disabled: StoryFn = ({ ...args }) => (
+
+
+ Disabled inactive
+ alert('Deleted')} />
+
+
+ Disabled active
+ alert('Deleted')} />
+
+
+)
+Disabled.parameters = {
+ docs: {
+ description: {
+ story:
+ 'A disabled chip can be active or not. OnClick inside a `Chip.Icon` is deactivated if the chip is disabled',
+ },
+ },
+}
diff --git a/packages/ui/src/components/Chip/__stories__/Groups.stories.tsx.tsx b/packages/ui/src/components/Chip/__stories__/Groups.stories.tsx.tsx
new file mode 100644
index 0000000000..f4bb99ad90
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Groups.stories.tsx.tsx
@@ -0,0 +1,122 @@
+import type { StoryFn } from '@storybook/react'
+import { useState } from 'react'
+import { Chip } from '..'
+import { Stack } from '../../Stack'
+import { Text } from '../../Text'
+
+export const Groups: StoryFn = ({ ...args }) => {
+ const [singleSelected, setSingleSelected] = useState(-1)
+ const [multiSelected, setMultiSelected] = useState([])
+
+ return (
+
+
+
+ Single-select group
+
+
+
+ singleSelected === 0
+ ? setSingleSelected(-1)
+ : setSingleSelected(0)
+ }
+ >
+ All
+
+
+ singleSelected === 1
+ ? setSingleSelected(-1)
+ : setSingleSelected(1)
+ }
+ >
+ Product
+
+
+ singleSelected === 2
+ ? setSingleSelected(-1)
+ : setSingleSelected(2)
+ }
+ >
+ Actions
+
+
+ singleSelected === 3
+ ? setSingleSelected(-1)
+ : setSingleSelected(3)
+ }
+ >
+ Resources
+
+
+ Selected chip: {singleSelected === -1 ? 'none' : singleSelected}
+
+
+
+ Muli-select group
+
+
+
+ multiSelected.includes(0)
+ ? setMultiSelected([])
+ : setMultiSelected([...multiSelected, 0])
+ }
+ >
+ All (18)
+
+
+ multiSelected.includes(1)
+ ? setMultiSelected(multiSelected.filter(id => id !== 1))
+ : setMultiSelected([...multiSelected, 1])
+ }
+ >
+ Product (2)
+
+
+ multiSelected.includes(2)
+ ? setMultiSelected(multiSelected.filter(id => id !== 2))
+ : setMultiSelected([...multiSelected, 2])
+ }
+ >
+ Actions (4)
+
+
+ multiSelected.includes(3)
+ ? setMultiSelected(multiSelected.filter(id => id !== 3))
+ : setMultiSelected([...multiSelected, 3])
+ }
+ >
+ Resources (12)
+
+
+ Selected chip{multiSelected.length > 1 ? 's' : null}:{' '}
+ {multiSelected.includes(0)
+ ? `1 2 3`
+ : multiSelected.map(id => `${id} `)}
+
+
+ )
+}
diff --git a/packages/ui/src/components/Chip/__stories__/Icons.stories.tsx b/packages/ui/src/components/Chip/__stories__/Icons.stories.tsx
new file mode 100644
index 0000000000..8852c9156a
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Icons.stories.tsx
@@ -0,0 +1,24 @@
+import type { StoryFn } from '@storybook/react'
+import { Chip } from '..'
+import { Stack } from '../../Stack'
+
+export const Icons: StoryFn = ({ ...args }) => (
+
+
+ Trailing icon
+ alert('Deleted')} />
+
+
+
+ Leading icon
+
+
+)
+Icons.parameters = {
+ docs: {
+ description: {
+ story:
+ 'To add an icon on the chip, use `Chip.Icon` inside the children of `Chip`.',
+ },
+ },
+}
diff --git a/packages/ui/src/components/Chip/__stories__/Playground.stories.tsx b/packages/ui/src/components/Chip/__stories__/Playground.stories.tsx
new file mode 100644
index 0000000000..1bb1300883
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Playground.stories.tsx
@@ -0,0 +1,3 @@
+import { Template } from './Template.stories'
+
+export const Playground = Template.bind({})
diff --git a/packages/ui/src/components/Chip/__stories__/Size.stories.tsx b/packages/ui/src/components/Chip/__stories__/Size.stories.tsx
new file mode 100644
index 0000000000..679457bf0a
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Size.stories.tsx
@@ -0,0 +1,14 @@
+import type { StoryFn } from '@storybook/react'
+import { Chip } from '..'
+import { Stack } from '../../Stack'
+
+export const Size: StoryFn = ({ ...args }) => (
+
+
+ Medium (default)
+
+
+ Large
+
+
+)
diff --git a/packages/ui/src/components/Chip/__stories__/Template.stories.tsx b/packages/ui/src/components/Chip/__stories__/Template.stories.tsx
new file mode 100644
index 0000000000..bd626fa4ec
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/Template.stories.tsx
@@ -0,0 +1,6 @@
+import type { StoryFn } from '@storybook/react'
+import { Chip } from '..'
+
+export const Template: StoryFn = ({ ...args }) => (
+ Default text
+)
diff --git a/packages/ui/src/components/Chip/__stories__/index.stories.tsx b/packages/ui/src/components/Chip/__stories__/index.stories.tsx
new file mode 100644
index 0000000000..25ed36a0de
--- /dev/null
+++ b/packages/ui/src/components/Chip/__stories__/index.stories.tsx
@@ -0,0 +1,23 @@
+import type { Meta } from '@storybook/react'
+import { Chip } from '..'
+
+export default {
+ component: Chip,
+ subcomponents: {
+ 'Chip.Icon': Chip.Icon,
+ },
+ decorators: [
+ StoryComponent => (
+
+
+
+ ),
+ ],
+ title: 'Components/Badges/Chip',
+} as Meta
+
+export { Playground } from './Playground.stories'
+export { Icons } from './Icons.stories'
+export { Groups } from './Groups.stories.tsx'
+export { Size } from './Size.stories'
+export { Disabled } from './Disabled.stories'
diff --git a/packages/ui/src/components/Chip/__tests__/__snapshots__/index.test.tsx.snap b/packages/ui/src/components/Chip/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000..8b61c0eb29
--- /dev/null
+++ b/packages/ui/src/components/Chip/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,970 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Checkbox > renders correctly 1`] = `
+
+ .emotion-0 {
+ font-size: 12px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 16px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+
+
+`;
+
+exports[`Checkbox > renders correctly active 1`] = `
+
+ .emotion-0 {
+ font-size: 12px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 16px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+.emotion-4 {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="false"]:hover {
+ background-color: #d9dadd;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="true"]:hover {
+ background-color: #8c40ef;
+}
+
+.emotion-4[data-disabled="true"] {
+ cursor: not-allowed;
+}
+
+.emotion-6 {
+ vertical-align: middle;
+ fill: #ffffff;
+ height: 16px;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+}
+
+.emotion-6 .fillStroke {
+ stroke: #ffffff;
+ fill: none;
+}
+
+
+
+`;
+
+exports[`Checkbox > renders correctly active disabled 1`] = `
+
+ .emotion-0 {
+ font-size: 12px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 16px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+.emotion-4 {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="false"]:hover {
+ background-color: #d9dadd;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="true"]:hover {
+ background-color: #8c40ef;
+}
+
+.emotion-4[data-disabled="true"] {
+ cursor: not-allowed;
+}
+
+.emotion-6 {
+ vertical-align: middle;
+ fill: #f3f3f4;
+ height: 16px;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+}
+
+.emotion-6 .fillStroke {
+ stroke: #f3f3f4;
+ fill: none;
+}
+
+
+
+`;
+
+exports[`Checkbox > renders correctly disabled 1`] = `
+
+ .emotion-0 {
+ font-size: 12px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 16px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+.emotion-4 {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="false"]:hover {
+ background-color: #d9dadd;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="true"]:hover {
+ background-color: #8c40ef;
+}
+
+.emotion-4[data-disabled="true"] {
+ cursor: not-allowed;
+}
+
+.emotion-6 {
+ vertical-align: middle;
+ fill: #b5b7bd;
+ height: 16px;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+}
+
+.emotion-6 .fillStroke {
+ stroke: #b5b7bd;
+ fill: none;
+}
+
+
+
+`;
+
+exports[`Checkbox > renders correctly large 1`] = `
+
+ .emotion-0 {
+ font-size: 14px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 20px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+.emotion-4 {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="false"]:hover {
+ background-color: #d9dadd;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="true"]:hover {
+ background-color: #8c40ef;
+}
+
+.emotion-4[data-disabled="true"] {
+ cursor: not-allowed;
+}
+
+.emotion-6 {
+ vertical-align: middle;
+ fill: #3f4250;
+ height: 16px;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+}
+
+.emotion-6 .fillStroke {
+ stroke: #3f4250;
+ fill: none;
+}
+
+
+
+`;
+
+exports[`Checkbox > renders correctly wiht icon 1`] = `
+
+ .emotion-0 {
+ font-size: 12px;
+ font-family: Inter,Asap,sans-serif;
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 16px;
+ text-transform: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 8px;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding: 4px 16px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 16px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ background-color: #ffffff;
+ cursor: pointer;
+ border: 1px solid #e9eaeb;
+ text-align: center;
+ color: #3f4250;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.emotion-2[data-disabled="false"]:hover {
+ background-color: #e9eaeb;
+ border-color: #92959d;
+ color: #222638;
+}
+
+.emotion-2[data-disabled="true"] {
+ background-color: #f3f3f4;
+ border-color: #d9dadd;
+ color: #b5b7bd;
+ cursor: not-allowed;
+}
+
+.emotion-2[data-active="true"] {
+ background-color: #8c40ef;
+ border-color: #8c40ef;
+ color: #ffffff;
+}
+
+.emotion-2[data-active="true"][data-disabled="false"]:hover {
+ background-color: #792dd4;
+ border-color: #792dd4;
+ color: #f9f9fa;
+}
+
+.emotion-2[data-active="true"][data-disabled="true"] {
+ background-color: #e5dbfd;
+ border: none;
+}
+
+.emotion-2[data-size='medium'] {
+ height: 24px;
+ padding: 4px 12px;
+}
+
+.emotion-2[data-size='large'] {
+ height: 32px;
+ padding: 4px 16px;
+}
+
+.emotion-2[data-trailing-icon="true"] {
+ padding-right: 8px;
+}
+
+.emotion-4 {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="false"]:hover {
+ background-color: #d9dadd;
+}
+
+.emotion-4[data-has-onclick="true"][data-active="true"]:hover {
+ background-color: #8c40ef;
+}
+
+.emotion-4[data-disabled="true"] {
+ cursor: not-allowed;
+}
+
+.emotion-6 {
+ vertical-align: middle;
+ fill: #3f4250;
+ height: 16px;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+}
+
+.emotion-6 .fillStroke {
+ stroke: #3f4250;
+ fill: none;
+}
+
+
+
+`;
diff --git a/packages/ui/src/components/Chip/__tests__/index.test.tsx b/packages/ui/src/components/Chip/__tests__/index.test.tsx
new file mode 100644
index 0000000000..f2eadbcaf7
--- /dev/null
+++ b/packages/ui/src/components/Chip/__tests__/index.test.tsx
@@ -0,0 +1,94 @@
+import { screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { renderWithTheme, shouldMatchEmotionSnapshot } from '@utils/test'
+import { describe, expect, test, vi } from 'vitest'
+import { Chip } from '..'
+
+describe('Checkbox', () => {
+ test('renders correctly', () => shouldMatchEmotionSnapshot(test))
+ test('renders correctly wiht icon', () =>
+ shouldMatchEmotionSnapshot(
+
+ {}} />
+ test
+ ,
+ ))
+
+ test('renders correctly active', () =>
+ shouldMatchEmotionSnapshot(
+
+ test
+ ,
+ ))
+
+ test('renders correctly large', () =>
+ shouldMatchEmotionSnapshot(
+
+ test
+ ,
+ ))
+
+ test('renders correctly disabled', () =>
+ shouldMatchEmotionSnapshot(
+
+ test
+ ,
+ ))
+
+ test('renders correctly active disabled', () =>
+ shouldMatchEmotionSnapshot(
+
+ test
+ ,
+ ))
+
+ test('throw error when using Chip.Icon outside of Chip', () => {
+ expect(() => renderWithTheme()).toThrowError(
+ 'Chip.Icon can only be used inside a Chip component',
+ )
+ })
+
+ test('renders correctly onClick', async () => {
+ const mockOnClick1 = vi.fn()
+ const mockOnClick2 = vi.fn()
+ const mockOnClickIcon1 = vi.fn()
+ const mockOnClickIcon2 = vi.fn()
+
+ renderWithTheme(
+ <>
+
+ test
+
+
+
+ test Disabled
+
+
+ >,
+ )
+
+ const chip = screen.getByTestId('test')
+ await userEvent.click(chip)
+ expect(mockOnClick1).toHaveBeenCalledOnce()
+
+ const chipDisabled = screen.getByTestId('test-disabled')
+ await userEvent.click(chipDisabled)
+ expect(mockOnClick2).toHaveBeenCalledTimes(0)
+
+ const chipIcon = screen.getByTestId('test-icon')
+ await userEvent.click(chipIcon)
+ expect(mockOnClickIcon1).toHaveBeenCalledOnce()
+
+ const chipIconDisabled = screen.getByTestId('test-icon-disabled')
+ await userEvent.click(chipIconDisabled)
+ expect(mockOnClickIcon2).toHaveBeenCalledTimes(0)
+ })
+})
diff --git a/packages/ui/src/components/Chip/index.tsx b/packages/ui/src/components/Chip/index.tsx
new file mode 100644
index 0000000000..a523eedeaa
--- /dev/null
+++ b/packages/ui/src/components/Chip/index.tsx
@@ -0,0 +1,152 @@
+import styled from '@emotion/styled'
+import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
+import { Stack } from '../Stack'
+import { Text } from '../Text'
+import { ChipContext } from './ChipContext'
+import { ChipIcon } from './ChipIcon'
+
+const StyledContainer = styled(Stack)`
+ padding: ${({ theme }) => `${theme.space['0.5']} ${theme.space['2']} `};
+ display: flex;
+ border-radius: ${({ theme }) => theme.radii.xlarge};
+ width: fit-content;
+ background-color: ${({ theme }) => theme.colors.neutral.background};
+ cursor: pointer;
+ border: 1px solid ${({ theme }) => theme.colors.neutral.borderWeak};
+ text-align: center;
+ color: ${({ theme }) => theme.colors.neutral.text};
+ user-select: none;
+
+
+ &[data-disabled="false"]:hover {
+ background-color: ${({ theme }) => theme.colors.neutral.backgroundHover};
+ border-color: ${({ theme }) => theme.colors.neutral.borderStrongHover};
+ color: ${({ theme }) => theme.colors.neutral.textHover};
+
+ }
+
+ &[data-disabled="true"] {
+ background-color: ${({ theme }) => theme.colors.neutral.backgroundDisabled};
+ border-color: ${({ theme }) => theme.colors.neutral.borderWeakDisabled};
+ color: ${({ theme }) => theme.colors.neutral.textDisabled};
+ cursor: not-allowed;
+ }
+
+ &[data-active="true"]{
+ background-color: ${({ theme }) => theme.colors.primary.backgroundStrong};
+ border-color: ${({ theme }) => theme.colors.primary.backgroundStrong};
+ color: ${({ theme }) => theme.colors.neutral.textStronger};
+
+ &[data-disabled="false"]:hover{
+ background-color: ${({ theme }) => theme.colors.primary.backgroundStrongHover};
+ border-color: ${({ theme }) => theme.colors.primary.backgroundStrongHover};
+ color: ${({ theme }) => theme.colors.neutral.textStrongerHover};
+
+
+ }
+
+ &[data-disabled="true"] {
+ background-color: ${({ theme }) => theme.colors.primary.backgroundStrongDisabled};
+ border: none;
+ }
+ }
+
+ &[data-size='medium']{
+ ${({ theme }) => `
+ height: ${theme.space[3]};
+ padding: ${theme.space['0.5']} ${theme.space['1.5']};`}
+ }
+
+ &[data-size='large']{
+ ${({ theme }) => `
+ height: ${theme.space[4]};
+ padding: ${theme.space['0.5']} ${theme.space['2']};`}
+ }
+
+ &[data-trailing-icon="true"] {
+ padding-right: ${({ theme }) => theme.space[1]}
+ }
+ `
+type ChipType = {
+ children: ReactNode
+ size?: 'medium' | 'large'
+ disabled?: boolean
+ active?: boolean
+ className?: string
+ 'data-testid'?: string
+ onClick?: (active: boolean) => void
+}
+
+/**
+ * Chip component is used to display a clickable status or a label in a small container
+ */
+export const Chip = ({
+ children,
+ size = 'medium',
+ disabled = false,
+ active = false,
+ className,
+ 'data-testid': dataTestId,
+ onClick,
+}: ChipType) => {
+ const [isActive, setIsActive] = useState(active)
+ const [hasTrailingIcon, setTrailingIcon] = useState(false)
+ const chipRef = useRef(null) // ref to the parent container
+ const iconRef = useRef(null)
+ const prominence = useMemo(() => {
+ if (isActive) return 'stronger'
+ if (disabled) return 'weak'
+
+ return 'default'
+ }, [isActive, disabled])
+ const value = useMemo(
+ () => ({ isActive, disabled, iconRef }),
+ [isActive, disabled, iconRef],
+ )
+ useEffect(() => {
+ setIsActive(active)
+ }, [active])
+
+ useEffect(() => {
+ if (chipRef.current && iconRef.current) {
+ const lastChildNode = chipRef.current.lastChild
+
+ // Compare the last child element with iconRef.current to check if the last element is an Icon
+ // This will mean that there is a trailing icon
+ if (lastChildNode === iconRef.current) {
+ setTrailingIcon(true)
+ } else setTrailingIcon(false)
+ }
+ }, [children, iconRef])
+
+ return (
+
+
+ {
+ if (!disabled) {
+ setIsActive(!isActive)
+ onClick?.(!isActive)
+ }
+ }}
+ className={className}
+ data-active={isActive}
+ data-testid={dataTestId}
+ alignItems="center"
+ justifyContent="center"
+ data-disabled={disabled}
+ data-prominence={prominence}
+ direction="row"
+ gap={1}
+ ref={chipRef}
+ data-trailing-icon={hasTrailingIcon}
+ >
+ {children}
+
+
+
+ )
+}
+
+Chip.Icon = ChipIcon
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 396ac18e61..78dd7355b1 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -13,6 +13,7 @@ export { Card } from './Card'
export { Carousel } from './Carousel'
export { Checkbox } from './Checkbox'
export { CheckboxGroup, CheckboxGroupCheckbox } from './CheckboxGroup'
+export { Chip } from './Chip'
export { CopyButton } from './CopyButton'
export { DateInput } from './DateInput'
export { Dialog } from './Dialog'