Skip to content

Commit

Permalink
Create ComboBox component(s) and story (#154)
Browse files Browse the repository at this point in the history
* WIP

* Add `trigger-width` spacing to Tailwind config

* WIP

* WIP

* WIP

* WIP

* Remove unused delay

* Merge
  • Loading branch information
psirenny authored Mar 25, 2024
1 parent 496cf89 commit 88546d6
Show file tree
Hide file tree
Showing 18 changed files with 2,233 additions and 235 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-trainers-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spear-ai/ui": minor
---

Added `cx` helper function to merge class names.
5 changes: 5 additions & 0 deletions .changeset/little-fireants-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spear-ai/ui": minor
---

Added ComboBox component.
5 changes: 5 additions & 0 deletions .changeset/orange-horses-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spear-ai/tailwind-config": patch
---

Reordered Tailwind plugins so that React Aria Components has the highest priority.
5 changes: 5 additions & 0 deletions .changeset/rotten-bulldogs-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spear-ai/ui": major
---

Bumped placeholder color from step 9 to step 10.
5 changes: 5 additions & 0 deletions .changeset/twenty-walls-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spear-ai/tailwind-config": minor
---

Added `trigger-width` spacing to Tailwind config.
2 changes: 1 addition & 1 deletion packages/tailwind-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const tailwindConfig: Config = {
containerQueriesPlugin,
formsPlugin,
radixColorThemePlugin,
reactAriaComponentsPlugin,
scrollbarPlugin,
threeDPlugin,
typographyPlugin,
reactAriaComponentsPlugin,
],
theme: {
colors,
Expand Down
10 changes: 8 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@
},
"type": "module",
"dependencies": {
"@leeoniya/ufuzzy": "1.0.14",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-slot": "1.0.2",
"@react-hookz/web": "24.0.4",
"@sentry/nextjs": "7.102.1",
"@spear-ai/logo": "2.1.1",
"autoprefixer": "10.4.18",
"classix": "2.1.36",
"graphql-relay": "0.10.0",
"next": "14.1.2",
"next-themes": "0.2.1",
"react": "18.2.0",
"react-aria-components": "1.1.1",
"react-dom": "18.2.0",
"react-hook-form": "7.51.0",
"react-intl": "6.6.2",
"react-stately": "3.30.1",
"tailwind-merge": "2.2.1",
"ts-invariant": "0.10.3",
"turbo": "1.12.4"
"use-debounce": "10.0.0"
},
"devDependencies": {
"@chromatic-com/storybook": "1.2.3",
Expand All @@ -47,6 +51,7 @@
"@types/node": "20.11.24",
"@types/react": "18.2.63",
"@types/react-dom": "18.2.19",
"autoprefixer": "10.4.18",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"graphql": "16.8.1",
Expand All @@ -55,6 +60,7 @@
"prettier": "3.2.5",
"storybook": "8.0.0-rc.1",
"tailwindcss": "3.4.1",
"turbo": "1.12.4",
"typescript": "5.3.3"
},
"license": "UNLICENSED",
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/checkbox/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const PreviewCheckbox = (properties: {
isInvalid={firstOptionIsInvalid}
value="isLimitedToFriends"
>
<div className="relative me-3 inline-flex size-4 items-center rounded border border-neutral-a-7 bg-white-a-3 text-neutral-12 group-invalid:border-x-negative-a-7 group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-primary-7 group-selected:border-transparent group-selected:bg-primary-9 group-selected:text-primary-contrast group-invalid:group-selected:border-x-negative-a-7 group-disabled:border-neutral-a-6 group-disabled:bg-neutral-a-3 group-disabled:text-neutral-a-8 group-invalid:group-disabled:border-x-negative-a-6 theme-dfs:bg-canvas-1 theme-dfs:group-invalid:border-x-negative-a-7 theme-dfs:group-disabled:bg-neutral-a-3 theme-forerunner:group-selected:bg-black-a-12 theme-forerunner:group-selected:text-white theme-forerunner:group-disabled:border-neutral-a-6 theme-forerunner:group-disabled:bg-neutral-a-3 theme-forerunner:group-disabled:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:border-x-negative-a-6 theme-galapago:bg-white dark:bg-white-a-3 theme-dfs:dark:bg-white-a-3 theme-dfs:group-selected:dark:border-neutral-a-7 theme-dfs:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:dark:bg-black-a-3 theme-forerunner:group-selected:dark:border-neutral-a-7 theme-forerunner:group-selected:dark:bg-primary-a-5 theme-forerunner:group-selected:dark:text-primary-12 theme-forerunner:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:group-disabled:dark:border-neutral-a-6 theme-forerunner:group-disabled:dark:bg-neutral-a-3 theme-forerunner:group-disabled:dark:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:dark:border-x-negative-a-6 theme-galapago:dark:bg-black-a-3 theme-galapago:dark:group-selected:bg-primary-a-9 theme-galapago:group-disabled:dark:bg-neutral-a-3 theme-galapago:group-disabled:dark:text-neutral-a-8 theme-galapago:dark:disabled:bg-neutral-a-3">
<div className="relative me-3 inline-flex size-4 items-center rounded border border-neutral-a-7 bg-white-a-3 text-neutral-12 group-invalid:border-x-negative-a-7 group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-primary-7 group-selected:border-transparent group-selected:bg-primary-9 group-selected:text-primary-contrast group-invalid:group-selected:border-x-negative-a-7 group-disabled:border-neutral-a-6 group-disabled:bg-neutral-a-3 group-disabled:text-neutral-a-8 group-invalid:group-disabled:border-x-negative-a-6 theme-dfs:bg-canvas-1 theme-dfs:group-invalid:border-x-negative-a-7 theme-dfs:group-disabled:bg-neutral-a-3 theme-forerunner:group-selected:bg-black-a-12 theme-forerunner:group-selected:text-white theme-forerunner:group-disabled:border-neutral-a-6 theme-forerunner:group-disabled:bg-neutral-a-3 theme-forerunner:group-disabled:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:border-x-negative-a-6 theme-galapago:bg-white dark:bg-white-a-3 theme-dfs:dark:bg-white-a-3 theme-dfs:group-selected:dark:border-neutral-a-7 theme-dfs:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:dark:bg-black-a-3 theme-forerunner:group-selected:dark:border-neutral-a-7 theme-forerunner:group-selected:dark:bg-primary-a-5 theme-forerunner:group-selected:dark:text-primary-12 theme-forerunner:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:group-disabled:dark:border-neutral-a-6 theme-forerunner:group-disabled:dark:bg-neutral-a-3 theme-forerunner:group-disabled:dark:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:dark:border-x-negative-a-6 theme-galapago:dark:bg-black-a-3 theme-galapago:dark:group-selected:bg-primary-a-9 theme-galapago:dark:disabled:bg-neutral-a-3 theme-galapago:group-disabled:dark:bg-neutral-a-3 theme-galapago:group-disabled:dark:text-neutral-a-8">
<CheckIcon className="absolute -left-px -top-px size-4 opacity-0 group-indeterminate:!opacity-0 group-selected:opacity-100" />
<MinusIcon className="h-full opacity-0 group-indeterminate:group-selected:opacity-100" />
</div>
Expand All @@ -90,7 +90,7 @@ const PreviewCheckbox = (properties: {
isIndeterminate={isIndeterminateWhenSelected ? true : undefined}
value="isShareable"
>
<div className="relative me-3 inline-flex size-4 items-center rounded border border-neutral-a-7 bg-white-a-3 text-neutral-12 group-invalid:border-x-negative-a-7 group-invalid:bg-x-negative-a-3 group-invalid:text-x-negative-12 group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-primary-7 group-selected:border-transparent group-selected:bg-primary-9 group-selected:text-primary-contrast group-invalid:group-selected:bg-x-negative-9 group-invalid:group-selected:text-x-negative-contrast group-disabled:border-neutral-a-6 group-disabled:bg-neutral-a-3 group-disabled:text-neutral-a-8 group-invalid:group-disabled:border-x-negative-a-6 group-invalid:group-disabled:bg-x-negative-a-3 group-invalid:group-disabled:text-x-negative-a-8 theme-dfs:bg-canvas-1 theme-dfs:group-invalid:border-x-negative-a-7 theme-dfs:group-invalid:bg-canvas-1 theme-dfs:group-invalid:text-x-negative-12 theme-dfs:group-disabled:bg-neutral-a-3 theme-dfs:group-invalid:group-disabled:bg-x-negative-a-3 theme-dfs:group-invalid:group-disabled:text-x-negative-a-8 theme-forerunner:group-selected:bg-black-a-12 theme-forerunner:group-selected:text-white theme-forerunner:group-invalid:group-selected:bg-x-negative-a-9 theme-forerunner:group-invalid:group-selected:text-x-negative-contrast theme-forerunner:group-disabled:border-neutral-a-6 theme-forerunner:group-disabled:bg-neutral-a-3 theme-forerunner:group-disabled:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:border-x-negative-a-6 theme-forerunner:group-invalid:group-disabled:bg-x-negative-a-3 theme-forerunner:group-invalid:group-disabled:text-x-negative-a-8 theme-galapago:bg-white dark:bg-white-a-3 theme-dfs:dark:bg-white-a-3 theme-dfs:group-invalid:dark:bg-x-negative-a-3 theme-dfs:group-selected:dark:border-neutral-a-7 theme-dfs:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:dark:bg-black-a-3 theme-forerunner:group-invalid:dark:bg-x-negative-a-3 theme-forerunner:group-selected:dark:border-neutral-a-7 theme-forerunner:group-selected:dark:bg-primary-a-5 theme-forerunner:group-selected:dark:text-primary-12 theme-forerunner:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:group-invalid:group-selected:dark:bg-x-negative-a-5 theme-forerunner:group-invalid:group-selected:dark:text-x-negative-12 theme-forerunner:group-disabled:dark:border-neutral-a-6 theme-forerunner:group-disabled:dark:bg-neutral-a-3 theme-forerunner:group-disabled:dark:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:dark:border-x-negative-a-6 theme-forerunner:group-invalid:group-disabled:dark:bg-x-negative-a-3 theme-forerunner:group-invalid:group-selected:group-disabled:dark:text-x-negative-a-8 theme-galapago:dark:bg-black-a-3 theme-galapago:dark:group-selected:bg-primary-a-9 theme-galapago:group-invalid:dark:group-selected:bg-x-negative-a-9 theme-galapago:group-disabled:dark:bg-neutral-a-3 theme-galapago:group-disabled:dark:text-neutral-a-8 theme-galapago:group-invalid:group-disabled:dark:bg-x-negative-a-3 theme-galapago:group-invalid:group-disabled:dark:text-x-negative-a-8 theme-galapago:dark:disabled:bg-neutral-a-3 theme-galapago:group-invalid:dark:disabled:bg-x-negative-a-3">
<div className="relative me-3 inline-flex size-4 items-center rounded border border-neutral-a-7 bg-white-a-3 text-neutral-12 group-invalid:border-x-negative-a-7 group-invalid:bg-x-negative-a-3 group-invalid:text-x-negative-12 group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-primary-7 group-selected:border-transparent group-selected:bg-primary-9 group-selected:text-primary-contrast group-invalid:group-selected:bg-x-negative-9 group-invalid:group-selected:text-x-negative-contrast group-disabled:border-neutral-a-6 group-disabled:bg-neutral-a-3 group-disabled:text-neutral-a-8 group-invalid:group-disabled:border-x-negative-a-6 group-invalid:group-disabled:bg-x-negative-a-3 group-invalid:group-disabled:text-x-negative-a-8 theme-dfs:bg-canvas-1 theme-dfs:group-invalid:border-x-negative-a-7 theme-dfs:group-invalid:bg-canvas-1 theme-dfs:group-invalid:text-x-negative-12 theme-dfs:group-disabled:bg-neutral-a-3 theme-dfs:group-invalid:group-disabled:bg-x-negative-a-3 theme-dfs:group-invalid:group-disabled:text-x-negative-a-8 theme-forerunner:group-selected:bg-black-a-12 theme-forerunner:group-selected:text-white theme-forerunner:group-invalid:group-selected:bg-x-negative-a-9 theme-forerunner:group-invalid:group-selected:text-x-negative-contrast theme-forerunner:group-disabled:border-neutral-a-6 theme-forerunner:group-disabled:bg-neutral-a-3 theme-forerunner:group-disabled:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:border-x-negative-a-6 theme-forerunner:group-invalid:group-disabled:bg-x-negative-a-3 theme-forerunner:group-invalid:group-disabled:text-x-negative-a-8 theme-galapago:bg-white dark:bg-white-a-3 theme-dfs:dark:bg-white-a-3 theme-dfs:group-invalid:dark:bg-x-negative-a-3 theme-dfs:group-selected:dark:border-neutral-a-7 theme-dfs:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:dark:bg-black-a-3 theme-forerunner:group-invalid:dark:bg-x-negative-a-3 theme-forerunner:group-selected:dark:border-neutral-a-7 theme-forerunner:group-selected:dark:bg-primary-a-5 theme-forerunner:group-selected:dark:text-primary-12 theme-forerunner:group-invalid:group-selected:dark:border-x-negative-a-7 theme-forerunner:group-invalid:group-selected:dark:bg-x-negative-a-5 theme-forerunner:group-invalid:group-selected:dark:text-x-negative-12 theme-forerunner:group-disabled:dark:border-neutral-a-6 theme-forerunner:group-disabled:dark:bg-neutral-a-3 theme-forerunner:group-disabled:dark:text-neutral-a-8 theme-forerunner:group-invalid:group-disabled:dark:border-x-negative-a-6 theme-forerunner:group-invalid:group-disabled:dark:bg-x-negative-a-3 theme-forerunner:group-invalid:group-selected:group-disabled:dark:text-x-negative-a-8 theme-galapago:dark:bg-black-a-3 theme-galapago:dark:group-selected:bg-primary-a-9 theme-galapago:group-invalid:dark:group-selected:bg-x-negative-a-9 theme-galapago:dark:disabled:bg-neutral-a-3 theme-galapago:group-invalid:dark:disabled:bg-x-negative-a-3 theme-galapago:group-disabled:dark:bg-neutral-a-3 theme-galapago:group-disabled:dark:text-neutral-a-8 theme-galapago:group-invalid:group-disabled:dark:bg-x-negative-a-3 theme-galapago:group-invalid:group-disabled:dark:text-x-negative-a-8">
<CheckIcon className="absolute -left-px -top-px size-4 opacity-0 group-indeterminate:!opacity-0 group-selected:opacity-100" />
<MinusIcon className="h-full opacity-0 group-indeterminate:group-selected:opacity-100" />
</div>
Expand Down
170 changes: 170 additions & 0 deletions packages/ui/src/components/combo-box/combo-box.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useControlledState } from "@react-stately/utils";
import type { Meta, StoryObj } from "@storybook/react";
import { useCallback, useMemo } from "react";
import { Form } from "react-aria-components";
import { useIntl } from "react-intl";
import {
ComboBox,
ComboBoxButton,
ComboBoxDescription,
ComboBoxFieldError,
ComboBoxIcon,
ComboBoxInput,
ComboBoxLabel,
ComboBoxListBox,
ComboBoxListBoxItem,
ComboBoxListBoxItemCheck,
ComboBoxListBoxItemCheckIcon,
ComboBoxListBoxItemLabel,
ComboBoxPopover,
ComboBoxTrigger,
} from "@/components/combo-box/combo-box";
import { querySensorConnection } from "@/data/sensor";

type SensorEdge = Awaited<ReturnType<typeof querySensorConnection>>["edges"][0];

const PreviewComboBox = (properties: {
hasLabel: boolean;
hasLabelDescription: boolean;
isAlwaysOpen: boolean;
isDisabled: boolean;
isInvalid: boolean;
isOptional: boolean;
isSquished: boolean;
menuTrigger: "focus" | "input";
}) => {
const {
hasLabel,
hasLabelDescription,
isAlwaysOpen,
isDisabled,
isInvalid,
isOptional,
isSquished,
menuTrigger,
} = properties;
const intl = useIntl();
const [selectedKey, setSelectedKey] = useControlledState<string | null>(undefined, null);
const [isOpen, setIsOpen] = useControlledState<boolean>(undefined, false);
const itemList = useMemo(() => {
const result = querySensorConnection({ first: 200 });
return isOptional ? [{ cursor: "", highlightedText: null, node: null }, ...result.edges] : result.edges;
}, [isOptional]);

const handleSelectionChange = useCallback(
(key: number | string) => {
if (isOptional && key === "") {
setSelectedKey(null);
setIsOpen(false);
} else {
setSelectedKey(`${key}`);
}
},
[isOptional, setIsOpen, setSelectedKey],
);

return (
<div className={`w-full ${isSquished ? "max-w-36" : "max-w-xs"}`}>
<Form className="relative w-full">
<ComboBox
className="w-full"
defaultItems={itemList}
isDisabled={isDisabled}
isInvalid={isInvalid}
menuTrigger={menuTrigger}
onOpenChange={setIsOpen}
onSelectionChange={handleSelectionChange}
selectedKey={selectedKey}
>
{hasLabel ? (
<ComboBoxLabel>
{intl.formatMessage({
defaultMessage: "Sensor",
id: "SCewMo",
})}
</ComboBoxLabel>
) : null}
{hasLabel && hasLabelDescription ? (
<ComboBoxDescription>
{intl.formatMessage({
defaultMessage: "A mechanical device sensitive to sound.",
id: "2YVoI/",
})}
</ComboBoxDescription>
) : null}
<ComboBoxTrigger>
<ComboBoxInput
placeholder={intl.formatMessage({
defaultMessage: "Select a sensor",
id: "W2C6Wt",
})}
/>
<ComboBoxButton>
<ComboBoxIcon />
</ComboBoxButton>
</ComboBoxTrigger>
{isInvalid ? (
<ComboBoxFieldError>
{intl.formatMessage({
defaultMessage: "Sensor is invalid.",
id: "JsiKrm",
})}
</ComboBoxFieldError>
) : null}
<ComboBoxPopover isOpen={isAlwaysOpen ? true : isOpen}>
<ComboBoxListBox>
{(edge: SensorEdge) => {
if (edge.cursor === "") {
return (
<ComboBoxListBoxItem id="" isNone>
{intl.formatMessage({
defaultMessage: "None",
id: "450Fty",
})}
</ComboBoxListBoxItem>
);
}

return edge.node == null ? null : (
<ComboBoxListBoxItem id={edge.node.id} key={edge.node.id} textValue={edge.node.name}>
<ComboBoxListBoxItemLabel>{edge.node.name}</ComboBoxListBoxItemLabel>
<ComboBoxListBoxItemCheck>
<ComboBoxListBoxItemCheckIcon />
</ComboBoxListBoxItemCheck>
</ComboBoxListBoxItem>
);
}}
</ComboBoxListBox>
</ComboBoxPopover>
</ComboBox>
</Form>
</div>
);
};

const meta = {
argTypes: {
menuTrigger: { control: { type: "select" }, options: ["focus", "input"] },
},
component: PreviewComboBox,
} satisfies Meta<typeof PreviewComboBox>;

type Story = StoryObj<typeof meta>;

export const Standard: Story = {
args: {
hasLabel: true,
hasLabelDescription: true,
isAlwaysOpen: false,
isDisabled: false,
isInvalid: false,
isOptional: true,
isSquished: false,
menuTrigger: "focus",
},
parameters: {
layout: "centered",
},
};

export default meta;
Loading

0 comments on commit 88546d6

Please sign in to comment.