Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
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
3 changes: 2 additions & 1 deletion app/client/packages/design-system/widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"@react-aria/visually-hidden": "^3.8.0",
"clsx": "^2.0.0",
"colorjs.io": "^0.4.3",
"lodash": "*"
"lodash": "*",
"react-aria-components": "^1.0.0-rc.0"
},
"devDependencies": {
"eslint-plugin-storybook": "^0.6.10"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./src";
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import clsx from "clsx";
import React from "react";
import {
Tag as HeadlessTag,
Button as HeadlessButton,
} from "react-aria-components";
import { getTypographyClassName } from "@design-system/theming";
import type { TagProps as HeadlessTagProps } from "react-aria-components";
Comment thread
KelvinOm marked this conversation as resolved.

import styles from "./styles.module.css";
import { CloseIcon } from "../../Modal/src/CloseIcon";
Comment thread
KelvinOm marked this conversation as resolved.

function Tag({ children, ...props }: HeadlessTagProps) {
const textValue = typeof children === "string" ? children : undefined;

return (
<HeadlessTag
textValue={textValue}
{...props}
className={clsx(styles["tag"], getTypographyClassName("footnote"))}
>
{({ allowsRemoving }) => (
<>
<span>{children}</span>
{allowsRemoving && (
<HeadlessButton slot="remove">
<CloseIcon />
</HeadlessButton>
)}
</>
)}
</HeadlessTag>
);
}

export { Tag };
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import {
Label as HeadlessLabel,
TagGroup as HeadlessTagGroup,
TagList as HeadlessTagList,
Text as HeadlessText,
} from "react-aria-components";
import type {
TagGroupProps as HeadlessTagGroupProps,
TagListProps as HeadlessTagListProps,
} from "react-aria-components";

import { Text } from "../../Text";
import styles from "./styles.module.css";
import { getTypographyClassName } from "@design-system/theming";

interface TagGroupProps<T>
extends Omit<HeadlessTagGroupProps, "children">,
Pick<HeadlessTagListProps<T>, "items" | "children" | "renderEmptyState"> {
label?: string;
description?: string;
errorMessage?: string;
}

function TagGroup<T extends object>(props: TagGroupProps<T>) {
const {
children,
description,
errorMessage,
items,
label,
renderEmptyState,
...rest
} = props;

return (
<HeadlessTagGroup {...rest} className={styles["tag-group"]}>
{Boolean(label) && <HeadlessLabel>{<Text>{label}</Text>}</HeadlessLabel>}
<HeadlessTagList
className={styles["tag-list"]}
items={items}
renderEmptyState={renderEmptyState}
>
{children}
</HeadlessTagList>
{Boolean(description) && (
<HeadlessText
className={getTypographyClassName("footnote")}
slot="description"
>
{description}
</HeadlessText>
)}
{Boolean(errorMessage) && (
<HeadlessText
className={getTypographyClassName("footnote")}
slot="errorMessage"
>
{errorMessage}
</HeadlessText>
)}
</HeadlessTagGroup>
);
}

export { TagGroup };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TagGroup } from "./TagGroup";
export { Tag } from "./Tag";
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.tag-group {
display: flex;
flex-direction: column;
gap: var(--inner-spacing-2);

/**
* ----------------------------------------------------------------------------
* ERROR MESSAGE
*-----------------------------------------------------------------------------
*/
[slot="errorMessage"] {
color: var(--color-fg-negative);
}
}

.tag-list {
display: flex;
flex-wrap: wrap;
gap: var(--inner-spacing-2);
}

.tag {
height: var(--sizing-6);
color: var(--color-fg);
background-color: var(--color-bg-neutral-subtle);
border-radius: var(--border-radius-1);
padding: 0 var(--inner-spacing-2);
outline: none;
cursor: default;
display: flex;
align-items: center;
transition: border-color 200ms;
overflow: hidden;

&:has([slot="remove"]) {
padding-inline-end: 0;
}

/**
* ----------------------------------------------------------------------------
* HOVERED
*-----------------------------------------------------------------------------
*/
&[data-hovered] {
background-color: var(--color-bg-neutral-subtle-hover);
}

&[data-focus-visible] {
outline: 2px solid var(--color-bd-focus);
outline-offset: 2px;
}

/**
* ----------------------------------------------------------------------------
* SELECTED TAG
*-----------------------------------------------------------------------------
*/
&[data-selected] {
border-color: var(--color-bd-neutral);
background: var(--color-bg-neutral);
color: var(--color-fg-on-neutral);
}

/**
* ----------------------------------------------------------------------------
* REMOVE BUTTON
*-----------------------------------------------------------------------------
*/
[slot="remove"] {
display: flex;
align-items: center;
justify-content: center;
height: var(--sizing-6);
width: var(--sizing-6);
background: none;
border: none;
padding: 0;
margin-inline-start: var(--inner-spacing-1);
color: var(--color-fg);
transition: color 200ms;
outline: none;
font-size: 0.95em;

&[data-hovered] {
background: var(--color-bg-neutral-subtle-hover);
}

svg {
width: 1em;
height: 1em;
}

&[data-hovered] {
color: var(--remove-button-color-hovered);
}
}

/**
* ----------------------------------------------------------------------------
* DISABLED TAG
*-----------------------------------------------------------------------------
*/
&[data-disabled] {
opacity: var(--opacity-disabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { TagGroup, Tag } from "@design-system/widgets";
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";

<Meta
title="Design-system/Widgets/TagGroup"
component={TagGroup}
args={{
onRemove: undefined,
children: (
<>
<Tag id="value-1">Value 1</Tag>
<Tag id="value-2">Value 2</Tag>
<Tag id="value-3">Value 3</Tag>
<Tag id="value-4">Value 4</Tag>
</>
),
}}
/>

export const Template = (args) => <TagGroup {...args} />;
Comment thread
KelvinOm marked this conversation as resolved.

# Tag Group

Tag Group is a group of checkboxes that can be selected together.

<Canvas>
<Story name="Tag Group">{Template.bind({})}</Story>
</Canvas>

## Props ( Tag Group )

<ArgsTable of={TagGroup} />

## Props ( Tag )

<ArgsTable of={Tag} />

# Single Selection Mode

Tag Group can be configured to allow only one selection at a time with the `selectionMode` prop.

<Canvas>
<Story
name="Single Selection Mode"
args={{
selectionMode: "single",
}}
>
{Template.bind({})}
</Story>
</Canvas>

# Multiple Selection Mode

Tag Group can be configured to allow multiple selections at a time with the `selectionMode` prop with value `multiple`.

<Canvas>
Comment thread
KelvinOm marked this conversation as resolved.
<Story
name="Multiple Selection Mode"
args={{
selectionMode: "multiple",
}}
>
{Template.bind({})}
</Story>
</Canvas>

## Removing Tags

Tags can be removed with the `onRemove` prop. The Tag will render a close icon when this prop is provided on the TagGroup.

<Canvas>
<Story
name="Removing Tags"
args={{
onRemove: (id) => {
console.log(id);
},
}}
>
{Template.bind({})}
</Story>
</Canvas>

## Disabled Tags

Tags can be disabled with the `disabledKeys` prop.

<Canvas>
<Story
name="Disabled Tags"
args={{
selectionMode: "multiple",
disabledKeys: ["value-2"],
}}
>
{Template.bind({})}
</Story>
</Canvas>

## Empty State

<Canvas>
<Story
name="Empty State"
args={{
renderEmptyState: () => "No categories.",
children: undefined,
}}
>
{Template.bind({})}
</Story>
</Canvas>

## With Label

<Canvas>
<Story
name="With Label"
args={{
label: "Categories",
selectionMode: "multiple",
}}
>
{Template.bind({})}
</Story>
</Canvas>

## With Description

<Canvas>
<Story
name="With Description"
args={{
label: "Categories",
description: "Select one or more categories.",
selectionMode: "multiple",
}}
>
{Template.bind({})}
</Story>
</Canvas>

## With Error

<Canvas>
<Story
name="With Error"
args={{
label: "Categories",
selectionMode: "multiple",
errorMessage: "Please select at least one category.",
}}
>
{Template.bind({})}
</Story>
</Canvas>
1 change: 1 addition & 0 deletions app/client/packages/design-system/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./components/TextArea";
export * from "./components/Spinner";
export * from "./components/Menu";
export * from "./components/Modal";
export * from "./components/TagGroup";

export * from "./utils";
export * from "./styles";
Expand Down
Loading