Skip to content

Commit

Permalink
Created a multiple select field.
Browse files Browse the repository at this point in the history
  • Loading branch information
ser888gio committed Nov 7, 2024
1 parent 497a4ff commit 612665a
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 35 deletions.
17 changes: 0 additions & 17 deletions apps/design-system/stories/multipleOption.stories.tsx

This file was deleted.

17 changes: 17 additions & 0 deletions apps/design-system/stories/multipleOptionSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MultipleSelectField } from "../../../packages/design-system/src/ui/input/combobobx/multipleSelect/multipleSelectField";

const meta: Meta<typeof MultipleSelectField> = {
title: "Components/MultipleSelectField",
component: MultipleSelectField,
argTypes: {},
tags: ["autodocs"],
};

type MultipleSelectFieldStory = StoryObj<typeof meta>;

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

export default meta;
12 changes: 6 additions & 6 deletions apps/design-system/stories/select-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SelectInputField } from "../../../packages/design-system/src/ui/input/selectField";
import { SingleSelectField } from "../../../packages/design-system/src/ui/input/combobobx/singleSelect/singleSelectField";
import { SearchIcon } from "@repo/design-system/demo";
import { HomeIcon } from "@repo/design-system/demo";

const meta: Meta<typeof SelectInputField> = {
title: "Components/SelectInput",
component: SelectInputField,
const meta: Meta<typeof SingleSelectField> = {
title: "Components/SingleSelectField",
component: SingleSelectField,
argTypes: {
label: { control: "text" },
error: { control: "text" },
Expand All @@ -26,9 +26,9 @@ const meta: Meta<typeof SelectInputField> = {
tags: ["autodocs"],
};

type SelectInputStory = StoryObj<typeof meta>;
type SingleSelectFieldStory = StoryObj<typeof meta>;

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

Expand Down
8 changes: 8 additions & 0 deletions packages/design-system/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@ module.exports = {
env: {
node: true,
},
rules: {
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
],
},
};
2 changes: 1 addition & 1 deletion packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"scripts": {
"build": "npm run fonts && npm run tailwind",
"dev": "npm run fonts && npm run tailwind:watch",
"fonts": "mkdir -p ./dist && cp -r ./src/fonts ./dist/fonts",
"fonts": "if not exist .\\dist mkdir .\\dist && xcopy .\\src\\fonts .\\dist\\fonts /E /I",
"_comment": "Changed the code from line 14, which is a script for fonts from {mkdir -p ./dist && cp -r ./src/fonts ./dist/fonts} to use on Windows {if not exist .\\dist mkdir .\\dist && xcopy .\\src\\fonts .\\dist\\fonts /E /I}",
"format": "prettier . --check",
"format:fix": "prettier . --write",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { forwardRef } from "react";
import { Description } from "./../description";
import { Field } from "./../field";
import { Combobox, Input } from "./../combobobx/combobox";
import { Combobox, Input } from "./singleSelect/combobox";
import { Label } from "./../label";
import { twMerge } from "tailwind-merge";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import {
Combobox as ComboboxPrimitive,
ComboboxInput as InputPrimitive,
ComboboxInputProps as InputPrimitiveProps,
ComboboxOptions as OptionsPrimitive,
ComboboxButton as ButtonPrimitive,
ComboboxButtonProps as ButtonProps,
} from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import * as React from "react";
import {
forwardRef,
useRef,
useImperativeHandle,
useState,
useEffect,
} from "react";
import { ClearButton } from "../../clearButton";
import { ChevronDownIcon } from "../../../../icons/chevronDown";
import { Option } from "../option";

interface ComboboxProps
extends React.ComponentPropsWithoutRef<typeof ComboboxPrimitive> {
showClearButton?: boolean;
onClear?: () => void;
defaultValue?: string[];
onInputChange?: (value: string[]) => void;

Check warning on line 27 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

'value' is defined but never used
options: Array<{ id: string | number; value: string; label: string }>;
showPlaceholder?: boolean;
children?: React.ReactNode;
}

const Combobox = forwardRef<
React.ElementRef<typeof ComboboxPrimitive>,
ComboboxProps
>(
(
{
className,
children,
showClearButton,
onChange,
defaultValue,
onClear,
onInputChange,
options,
...props
},
ref

Check warning on line 49 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
) => {
//Use state for selecting values. If nothing is provided, defaultValue will become the selected value
const [selectedValues, setSelectedValues] = useState<string[]>(
defaultValue instanceof Array
? defaultValue
: defaultValue
? [defaultValue]
: []

Check warning on line 57 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);

const [query, setQuery] = useState<string>(
defaultValue ? defaultValue.join(", ") : ""

Check warning on line 61 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);
const [filteredOptions, setFilteredOptions] = useState(options);

useEffect(() => {
const filtered = options.filter((option) =>
option.label.toLowerCase().includes(query.toLowerCase())

Check warning on line 67 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);
setFilteredOptions(filtered);
}, [query, options]);

const handleClear = () => {
setSelectedValues([]);
setQuery("");
if (onChange) {
onChange([]);
}
if (onClear) {
onClear();
}
if (onInputChange) {
onInputChange([]);
}
};

const handleChange = (value: string | null) => {
setSelectedValues(value ? [value] : []);
if (value) {
const selectedOption = options.find((opt) => opt.value === value);
if (selectedOption) {
setQuery(selectedOption.label);
if (onInputChange) {
onInputChange([selectedOption.label]);
}
}
}
if (onChange) {
onChange(value);
}
};

//Takes an input change event as an argument. Extracts the new value from the input element. Updates the state with the new value. Optionally calls a provided callback function with the new value.
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setQuery(value);
if (onInputChange) {
onInputChange([value]);
}
};

return (
<ComboboxPrimitive
multiple
as="div"
ref={ref}
className={twMerge("k1-relative k1-w-full", className)}
onChange={handleChange}
value={selectedValues}
{...props}
>
<div className="k1-flex k1-items-center k1-w-full">
<Input
onChange={handleInputChange}
className="k1-flex-grow k1-container k1-peer"
value={
selectedValues.length > 0 ? selectedValues.join(", ") : query
}
data-focus={query ? "true" : undefined}
displayValue={(value: string) => {
const option = options.find((opt) => opt.value === value);
return option ? option.label : query;
}}
/>
{children}
<Button className="k1-flex-shrink-0 k1-h-full k1-flex k1-items-center k1-pr-2">
<ChevronDownIcon className="k1-h-6 k1-w-6" />
</Button>
{showClearButton && <ClearButton onClose={handleClear} />}
</div>
<Options anchor="bottom start">
{filteredOptions.length === 0 ? (
<div className="k1-px-4 k1-py-2">Žadné vysledky</div>
) : (
filteredOptions.map((option) => (
<Option
key={option.id}
value={option.value}
className={twMerge("", className)}
>
{option.label}
</Option>
))
)}
</Options>
</ComboboxPrimitive>
);
}

Check warning on line 157 in packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);

const Input = forwardRef<
React.ElementRef<typeof InputPrimitive>,
InputPrimitiveProps & { className?: string } // InputProps has more variable className, but we need string
>(({ className, ...props }, ref) => {
const inputRef = useRef<HTMLInputElement | null>(null);

useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);

return (
<InputPrimitive
ref={inputRef}
className={twMerge(
"w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm",
className
)}
{...props}
/>
);
});
Input.displayName = InputPrimitive.displayName;

const Options = forwardRef<
React.ElementRef<typeof OptionsPrimitive>,
React.ComponentPropsWithoutRef<typeof OptionsPrimitive>
>(({ className, children, ...props }, ref) => (
<OptionsPrimitive
ref={ref}
className={twMerge(
"k1-bg-white k1-rounded-tl-lg k1-border k1-border-neutral k1-flex-col k1-justify-start k1-items-start k1-inline-flex",
className
)}
{...props}
>
{children}
</OptionsPrimitive>
));
Options.displayName = "Combobox.Options";

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, Button };
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { forwardRef } from "react";
import { Description } from "../../description";
import { Field } from "../../field";
import { Combobox, Input } from "./combobox";
import { Label } from "../../label";
import { twMerge } from "tailwind-merge";

/*
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;

// Test options
const options = [
{ id: 1, value: "option1", label: "Option 1" },
{ id: 2, value: "option2", label: "Option 2" },
{ id: 3, value: "option3", label: "Option 3" },
{ id: 4, value: "option4", label: "Option 4" },
];

const MultipleSelectField = forwardRef<React.ElementRef<typeof Input>, Props>(
({ label, error, showClearButton, icon }: Props, ref) => {
// 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
className="k1-relative k1-w-full k1-bg-transparent k1-outline-none k1-flex"
ref={ref}
showClearButton={showClearButton}
options={options}
defaultValue={[]}
onChange={(value) => console.log("Selected value:", value)}
onInputChange={(value) => console.log("Input value:", value)}
onClear={() => console.log("Cleared")}
showPlaceholder={showPlaceholder}
>
{label && (
<Label state={hasError ? "error" : "default"} hasIcon={hasIcon}>
{label}
</Label>
)}
</Combobox>
{hasError && <Description state="error">{error}</Description>}
</Field>
);
}
);

export { MultipleSelectField };
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Option = forwardRef<
<OptionPrimitive
ref={ref}
className={twMerge(
"k1-px-4 k1-py-3 k1-border-b k1-border-neutral k1-justify-start k1-items-start k1-inline-flex hover:k1-bg-neutral-backdrop-hover active:k1-bg-neutral-backdrop-active k1-text-neutral-fg k1-text-base",
"k1-px-4 k1-py-3 k1-border-b k1-border-neutral k1-justify-start k1-items-start k1-inline-flex hover:k1-bg-neutral-backdrop-hover active:k1-bg-neutral-backdrop-active k1-text-neutral-fg k1-text-base k1-w-full",
className
)}
{...props}
Expand Down
Loading

0 comments on commit 612665a

Please sign in to comment.