Skip to content

Commit

Permalink
Created a Select Input field. Need to add styling.
Browse files Browse the repository at this point in the history
  • Loading branch information
ser888gio committed Oct 25, 2024
1 parent d087fe4 commit 20f7897
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 0 deletions.
23 changes: 23 additions & 0 deletions apps/design-system/stories/select-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SelectInputField } from "../../../packages/design-system/src/ui/input/selectField";
import { SearchIcon } from "@repo/design-system/demo";
import { HomeIcon } from "@repo/design-system/demo";

const meta: Meta<typeof SelectInputField> = {
title: "Components/SelectInput",
component: SelectInputField,
argTypes: {
label: { control: "text" },
error: { control: "text" },
showClearButton: { control: "boolean" },
},
tags: ["autodocs"],
};

type SelectInputStory = StoryObj<typeof meta>;

export const Default: SelectInputStory = {
args: {},
};

export default meta;
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packages/*"
],
"dependencies": {
"@headlessui/react": "^2.1.10",
"class-variance-authority": "^0.7.0"
}
}
20 changes: 20 additions & 0 deletions packages/design-system/src/icons/ChevronDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from "react";

export function ChevronDownIcon(
props: React.JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>

Check warning on line 4 in packages/design-system/src/icons/ChevronDown.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
fill="none"
{...props}
>
{props.children}
<path
fill="#565252"
d="M6 7.05.35 1.375 1.4.325l4.6 4.6 4.6-4.6 1.05 1.05L6 7.05Z"
/>
</svg>
);
}
1 change: 1 addition & 0 deletions packages/design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./icons/ClearIcon";
export * from "./icons/SearchIcon";
export * from "./icons/HomeIcon";
export * from "./icons/ErrorIcon";
export * from "./icons/ChevronDown";
106 changes: 106 additions & 0 deletions packages/design-system/src/ui/input/combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
Combobox as ComboboxPrimitive,
ComboboxInput as InputPrimitive,
ComboboxInputProps as InputPrimitiveProps,
ComboboxOptions as OptionsPrimitive,
ComboboxOption as OptionPrimitive,
ComboboxButton as ButtonPrimitive,
ComboboxButtonProps as ButtonProps,
} from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import * as React from "react";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { ClearButton } from "./clearButton";

const Combobox = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive>
>(({ className, children, ...props }, ref) => (
<ComboboxPrimitive
as="div"
ref={ref}
className={twMerge("", className)}
{...props}
>
{children}
</ComboboxPrimitive>
));
Combobox.displayName = "Combobox";

const Input = forwardRef<
React.ElementRef<typeof InputPrimitive>,
InputPrimitiveProps & { className?: string; showClearButton?: boolean } // InputProps has more variable className, but we need string
>(({ className, showClearButton, ...props }, ref) => {
// We need to get object ref we're passing to check if the input is empty
const inputRef = useRef<HTMLInputElement | null>(null);
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);

// Add a data attribute based on the input value. This is for peer styling.
const updateDataEmpty = () => {
if (inputRef.current) {
const isEmpty = inputRef.current.value === "";
inputRef.current.setAttribute("data-empty", isEmpty.toString());
}
};

// Override the onChange event to update data-empty attribute
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (props.onChange) {
props.onChange(event);
}
updateDataEmpty();
};

// Clear the input field and fire the onChange event
const clearHandler = () => {
if (inputRef.current) {
inputRef.current.value = "";
handleChange({
target: inputRef.current,
} as React.ChangeEvent<HTMLInputElement>);
}
};

return (
<>
<InputPrimitive
ref={inputRef}
className={twMerge("k1-peer k1-flex-grow", className)}
onChange={handleChange}
{...props}
/>
{showClearButton && <ClearButton onClose={clearHandler} />}
</>
);
});
Input.displayName = InputPrimitive.displayName;

const Options = forwardRef<
React.ElementRef<typeof OptionsPrimitive>,
React.ComponentPropsWithoutRef<typeof OptionsPrimitive>
>(({ className, children, ...props }, ref) => (
<OptionsPrimitive ref={ref} className={twMerge("", className)} {...props}>
{children}
</OptionsPrimitive>
));
Options.displayName = "Combobox.Options";

const Option = forwardRef<
React.ElementRef<typeof OptionPrimitive>,
React.ComponentPropsWithoutRef<typeof OptionPrimitive>
>(({ className, children, ...props }, ref) => (
<OptionPrimitive ref={ref} className={twMerge("", className)} {...props}>
{children}
</OptionPrimitive>
));
Option.displayName = "Combobox.Option";

const Button = forwardRef<
React.ElementRef<typeof ButtonPrimitive>,
ButtonProps & { className?: string }
>(({ className, ...props }, ref) => (
<ButtonPrimitive ref={ref} className={twMerge("", className)} {...props} />
));
Button.displayName = "Combobox.Button";

export { Combobox, Input, Options, Option, Button };
83 changes: 83 additions & 0 deletions packages/design-system/src/ui/input/selectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { forwardRef } from "react";
import { Description } from "./description";
import { Field } from "./field";
import { Combobox, Input, Options, Option, Button } from "./combobox";
import { Label } from "./label";
import { twMerge } from "tailwind-merge";
import { ChevronDownIcon } from "../../icons/ChevronDown";

/*
We choose what Input field properties we allow to be passed to the Input component
This is needed to allow to manage the Input field using things like ...register("field") react-form-hook
We omit className to disable extra styling from the outside
*/
type InheritedInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"className" | "style"
>;

type Props = {
label?: string;
error?: string;
showClearButton?: boolean;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
} & InheritedInputProps;

const SelectInputField = forwardRef<React.ElementRef<typeof Input>, Props>(
(
{ label, error, showClearButton, icon, placeholder, ...props }: Props,
ref

Check warning on line 30 in packages/design-system/src/ui/input/selectField.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
) => {
// if error is present, we pass it to all the sub-components
const hasError = !!error;

console.info("InputField", { hasError, label, error, showClearButton });

// We show label instead of placeholder if label is provided
const showPlaceholder = !label;

// We need to pass hasIcon to some sub-components
const Icon = icon;
const hasIcon = !!Icon;

return (
<Field state={hasError ? "error" : "default"}>
{hasIcon && <Icon className={twMerge("k1-w-6 k1-h-6 k1-min-w-6")} />}
<Combobox>
<Input
{...(props as Omit<InheritedInputProps, "defaultValue"> & {
defaultValue?: string;
})}
ref={ref}
className="k1-bg-transparent k1-outline-none"
placeholder={showPlaceholder ? placeholder : undefined}
showClearButton={showClearButton}
/>
<Button>
<ChevronDownIcon className="k1-w-6 k1-h-6 k1-min-w-6" />
</Button>
<Options>
<Option value="1">Option 1</Option>
<Option value="2">Option 2</Option>
<Option value="3">Option 3</Option>
</Options>
</Combobox>

{label && (
<Label state={hasError ? "error" : "default"} hasIcon={hasIcon}>
{label}
</Label>
)}
{hasError && (
<Description state="error">
{/* Custom error - must be text as in zod validator */}
{`Fix the ${label?.toLowerCase()}`}
</Description>
)}
</Field>
);
}

Check warning on line 80 in packages/design-system/src/ui/input/selectField.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);

export { SelectInputField };

0 comments on commit 20f7897

Please sign in to comment.