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

Add DropdownChipInput #1115

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/green-seas-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@asyncapi/studio-ui": patch
---

Add DropdownChipInput
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ build
dist
.turbo
.env
apps/design-system/src/styles/tailwind.output.css
2 changes: 1 addition & 1 deletion apps/design-system/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ storybook-static

npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*
28 changes: 28 additions & 0 deletions apps/design-system/src/components/Chip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Chip } from '@asyncapi/studio-ui';


const meta: Meta<typeof Chip> = {
component: Chip,
parameters: {
layout: "fullscreen",
backgrounds: {
default: "dark",
},
},
args: {
onDelete: fn()
}
}
export default meta;


type Story = StoryObj<typeof Chip>


export const Default: Story = {
args: {
chip: 'Chip',
},
}
61 changes: 61 additions & 0 deletions apps/design-system/src/components/DropdownChipInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Meta, StoryObj } from '@storybook/react';
import { DropdownChipInput } from '@asyncapi/studio-ui';
import { useState } from 'react';

const meta: Meta<typeof DropdownChipInput> = {
component: DropdownChipInput,
parameters: {
layout: "fullscreen",
backgrounds: {
default: "light",
},
}
}
export default meta;


type Story = StoryObj<typeof DropdownChipInput>

const OPTIONS = ["string", "number", "boolean", "object", "array", "null"];

export const Default: Story = {
render: function Render() {
const [currentChips, setCurrentChips] = useState<string[]>(["boolean"]);
return (
<DropdownChipInput
className='w-96'
chips={currentChips}
onChange={setCurrentChips}
chipsOptions={OPTIONS}/>
)
}
}


export const Disabled: Story = {
render: function Render() {
const [currentChips, setCurrentChips] = useState<string[]>(["string"]);
return (
<DropdownChipInput
className='w-96'
chips={currentChips}
onChange={setCurrentChips}
isDisabled={true}
chipsOptions={OPTIONS}/>
)
}
}

export const WithPlaceholder: Story = {
render: function Render() {
const [currentChips, setCurrentChips] = useState<string[]>([]);
return (
<DropdownChipInput
className='w-96'
placeholder='Add a chip'
chips={currentChips}
onChange={setCurrentChips}
chipsOptions={OPTIONS}/>
)
}
}
4 changes: 4 additions & 0 deletions apps/design-system/src/styles/tailwind.output.css
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,10 @@ video {
width: 100%;
}

.w-96 {
width: 24rem;
}

.grow {
flex-grow: 1;
}
Expand Down
32 changes: 32 additions & 0 deletions packages/ui/components/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react"
import { cn } from "@asyncapi/studio-utils"
interface ChipInputProps {
chip: string
onDelete: (chip: string) => void
}

export const Chip = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & ChipInputProps>(
({ chip, onDelete,className , ...props }, ref) => {
return (
<div
className={cn("m-1 w-fit bg-gray-100 text-gray-900 rounded px-2 py-1 flex items-center border border-gray-400 focus:border-blue-500 focus:border-2 focus:outline-none", className)}
style={{ height: "28px", borderStyle: "solid" }}
ref={ref}
{...props}
>
<span>{chip}</span>
<button
onClick={(e) => {
console.log("Delete button clicked")
onDelete(chip)
}}
tabIndex={-1}
className="ml-1 text-gray-400 focus:outline-none"
aria-label="Close"
>
×
</button>
</div>
)
}
)
15 changes: 3 additions & 12 deletions packages/ui/components/ChipInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FunctionComponent, KeyboardEvent, useRef } from 'react';
import { Chip } from './Chip';

interface ChipInputProps {
name: string;
Expand Down Expand Up @@ -44,25 +45,15 @@ export const ChipInput: FunctionComponent<ChipInputProps> = ({
}
};

const handleDelete = (chipToDelete: string) => () => {
const handleDelete = (chipToDelete: string) => {
const updatedChips = chips.filter(chip => chip !== chipToDelete);
onChange(updatedChips);
};

return (
<div className={`${className} flex flex-wrapitems-center p-1 bg-gray-900 rounded border border-gray-800`} style={{ width: '862px', height: '46px' }}>
{chips.map((chip, index) => (
<div
key={chip}
className="m-1 bg-gray-100 text-gray-900 rounded px-2 py-1 flex items-center border border-gray-400 focus:border-blue-500 focus:border-2 focus:outline-none"
style={{ height: '28px', borderStyle: 'solid' }}
tabIndex={0}
onKeyDown={handleChipKeyDown(index)}
ref={index === 0 ? firstChipRef : undefined}
>
<span>{chip}</span>
<button onClick={handleDelete(chip)} tabIndex={-1} className="ml-1 text-gray-400 focus:outline-none" aria-label='Close'>×</button>
</div>
<Chip key={chip} chip={chip} tabIndex={0} onDelete={handleDelete} onKeyDown={handleChipKeyDown(index)} ref={index === 0 ? firstChipRef : undefined} />
))}
<input
ref={inputRef}
Expand Down
75 changes: 75 additions & 0 deletions packages/ui/components/DropdownChipInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import { FunctionComponent, KeyboardEvent, useRef } from 'react';
import { Chip } from './Chip';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "./DropdownMenu"
import { PlusCircleIcon } from './icons';
import { cn } from '@asyncapi/studio-utils';


interface DropdownChipInputProps {
className?: string;
chips: string[];
chipsOptions: string[];
placeholder?: string;
onChange: (chips: string[]) => void;
isDisabled?: boolean;
}

export const DropdownChipInput: FunctionComponent<DropdownChipInputProps> = ({
className,
chips,
chipsOptions,
onChange,
placeholder,
isDisabled = false,
}) => {
const firstChipRef = useRef<HTMLDivElement>(null);


const onChipCheckedChange = (chip: string) => {
if (chips.includes(chip)) {
handleDelete(chip);
} else {
onChange([...chips, chip]);
}
}
const handleChipKeyDown = (index: number) => (event: KeyboardEvent<HTMLDivElement>) => {
event.stopPropagation();
if (event.key === 'Backspace') {
const updatedChips = [...chips];
updatedChips.splice(index, 1);
onChange(updatedChips);
}
};

const handleDelete = (chipToDelete: string) => {
const updatedChips = chips.filter(chip => chip !== chipToDelete);
onChange(updatedChips);
};

return (
<div className={cn("flex h-11 p-1 bg-gray-900 rounded border border-gray-800 items-center",isDisabled && "opacity-50 pointer-events-none", className)}>
{chips.length === 0 && placeholder && <span className='text-gray-500'>{placeholder}</span>}
{chips.map((chip, index) => (
<Chip key={chip} chip={chip} tabIndex={0} onDelete={handleDelete} onKeyDown={handleChipKeyDown(index)} ref={index === 0 ? firstChipRef : undefined} />
))}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<PlusCircleIcon className='min-w-7 w-7 h-7 min-h-7 text-gray-500 ml-auto ' />
</DropdownMenuTrigger>
<DropdownMenuContent>
{chipsOptions.map((chipOption) => (
<DropdownMenuCheckboxItem key={chipOption} checked={chips.includes(chipOption)} onCheckedChange={() => onChipCheckedChange(chipOption)}>
{chipOption}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
25 changes: 25 additions & 0 deletions packages/ui/components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"

import { cn } from "@asyncapi/studio-utils"
import { CheckIcon } from './icons'

const DropdownMenu = DropdownMenuPrimitive.Root

Expand Down Expand Up @@ -66,12 +67,36 @@ const DropdownMenuSeparator = React.forwardRef<
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName

const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"flex items-center gap-2 text-gray-200 text-sm leading-7 px-2 cursor-pointer rounded outline-none select-none hover:bg-gray-700 focus:bg-gray-700",
className
)}
checked={checked}
{...props}
>
<span className="left-2 flex h-3.5 w-3.5">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName

export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuSeparator,
DropdownMenuGroup,
DropdownMenuPortal,
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import './styles.css'

// components
export * from './Chip'
export * from './DropdownChipInput'
export * from './ChipInput'
export * from './EditorSwitch'
export * from './DropdownMenu'
Expand Down
Loading