Skip to content

Commit

Permalink
Add storybook to repo (#664)
Browse files Browse the repository at this point in the history
* wip storybook

* Configure button story

* Include classnames used in storybook in tailwind config

* Multivalue component (#667)

* Add story for MultiSelect

* Initial MultiValueInput

* Tag styling

* Sort components

* Add forwardRef to badge component

* Close tag

* Add storybook component test

* Add placeholder example

* CSS tweaks

* Add form usage

* Configure ui package with new component

* Add storybook to top level

* Add other stories to top level storybook

* Move react to dev dependency

* Uninstall storybook from packages/ui

* Use base ui package's tailwind theme

* Fix hover

* Remove storybook reference from packages/ui/tailwind.config.js

* Add readme
  • Loading branch information
allisonking authored Oct 2, 2024
1 parent e2e8d94 commit 62dad71
Show file tree
Hide file tree
Showing 18 changed files with 2,018 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ core/supabase/.temp
**/Brewfile.lock.json

.env.docker-compose

*storybook.log
storybook-static
4 changes: 4 additions & 0 deletions packages/ui/multivalue-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/ui-multivalue-input.cjs.js",
"module": "dist/ui-multivalue-input.esm.js"
}
8 changes: 8 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@
"module": "./pubFields/dist/ui-pubFields.esm.js",
"default": "./pubFields/dist/ui-pubFields.cjs.js"
},
"./multivalue-input": {
"module": "./multivalue-input/dist/ui-multivalue-input.esm.js",
"default": "./multivalue-input/dist/ui-multivalue-input.cjs.js"
},
"./customRenderers/confidence/confidence": {
"module": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.esm.js",
"default": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.cjs.js"
Expand Down Expand Up @@ -255,6 +259,7 @@
"label",
"menubar",
"multi-select",
"multivalue-input",
"navigation-menu",
"pagination",
"popover",
Expand Down Expand Up @@ -283,6 +288,8 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@hookform/resolvers": "^3.3.1",
"@lexical/code": "^0.15.0",
"@lexical/link": "^0.15.0",
Expand Down Expand Up @@ -408,6 +415,7 @@
"label.tsx",
"menubar.tsx",
"multi-select.tsx",
"multivalue-input.tsx",
"navigation-menu.tsx",
"pagination.tsx",
"popover.tsx",
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/src/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
({ className, variant, ...props }: BadgeProps, ref) => {
return <div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />;
}
);

export { Badge, badgeVariants };
135 changes: 135 additions & 0 deletions packages/ui/src/multivalue-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Adapted from https://gist.github.com/enesien/03ba5340f628c6c812b306da5fedd1a4

import type { Active, DragEndEvent } from "@dnd-kit/core";
import type { Dispatch, SetStateAction } from "react";

import React, { forwardRef, useState } from "react";
import { DndContext } from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";

import type { InputProps } from "./input";
import { Badge } from "./badge";
import { Button } from "./button";
import { GripVertical, XCircle } from "./icon";
import { Input } from "./input";

type MultiValueInputProps = Omit<InputProps, "onChange"> & {
value: string[];
onChange: Dispatch<SetStateAction<string[]>>;
};

const SortableValue = ({
value,
onRemove,
isActive,
}: {
value: string;
onRemove: (v: string) => void;
isActive: boolean;
}) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: value });

const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
transition,
zIndex: isActive ? 50 : undefined,
}
: undefined;

return (
<Badge
ref={setNodeRef}
style={style}
{...attributes}
className="bg-muted-foreground py-1"
data-testid={`sortable-value-${value}`}
>
<Button {...listeners} variant="ghost" className="mr-1 h-5 p-0">
<GripVertical size="12" />
</Button>
{value}
<Button
onClick={() => {
onRemove(value);
}}
variant="ghost"
// height is smaller so hover is only over the xcircle
className="ml-2 h-3 p-0"
data-testid="remove-button"
>
<XCircle size="12"></XCircle>
</Button>
</Badge>
);
};

export const MultiValueInput = forwardRef<HTMLInputElement, MultiValueInputProps>(
({ value: values, onChange, ...props }, ref) => {
const [pendingValue, setPendingValue] = useState("");
const [activeDrag, setActiveDrag] = useState<Active | null>(null);

const addPendingValue = () => {
if (pendingValue) {
const newValues = new Set([...values, pendingValue]);
onChange(Array.from(newValues));
setPendingValue("");
}
};

const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const activeIndex: number = values.findIndex((v) => v === active.id);
const overIndex: number = values.findIndex((v) => v === over.id);
onChange(arrayMove(values, activeIndex, overIndex));
}
setActiveDrag(null);
};

return (
<div className="isolate flex flex-col gap-2">
<Input
value={pendingValue}
onChange={(e) => setPendingValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
addPendingValue();
}
}}
placeholder="Type the value and hit enter"
{...props}
ref={ref}
data-testid="multivalue-input"
/>

<DndContext
onDragStart={({ active }) => {
setActiveDrag(active);
}}
onDragEnd={handleDragEnd}
>
<SortableContext items={values}>
<div className="flex flex-wrap gap-x-2 gap-y-2">
{values.map((value) => {
return (
<SortableValue
key={value}
value={value}
onRemove={(valueToRemove) =>
onChange(values.filter((v) => v !== valueToRemove))
}
isActive={activeDrag?.id === value}
/>
);
})}
</div>
</SortableContext>
</DndContext>
</div>
);
}
);

MultiValueInput.displayName = "MultiValueInput";
Loading

0 comments on commit 62dad71

Please sign in to comment.