Skip to content

Commit

Permalink
Started to develop a multi-select box with checkbox options.
Browse files Browse the repository at this point in the history
  • Loading branch information
ser888gio committed Nov 11, 2024
1 parent 9bfc7a4 commit c560605
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 30 deletions.
12 changes: 6 additions & 6 deletions apps/design-system/stories/checkboxOption.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CheckboxOption } from "../../../packages/design-system/src/ui/input/combobobx/multipleSelect/checkboxOption";
import { CheckboxSelectField } from "../../../packages/design-system/src/ui/input/combobobx/checkboxSelect/checkboxField";

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

type CheckboxOptionStory = StoryObj<typeof meta>;
type CheckboxSelectFieldStory = StoryObj<typeof meta>;

export const Default: CheckboxOptionStory = {
export const Default: CheckboxSelectFieldStory = {
args: {},
};

Expand Down
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 CheckboxSelectField = 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>
);
}

Check warning on line 71 in packages/design-system/src/ui/input/combobobx/checkboxSelect/checkboxField.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);

export { CheckboxSelectField };
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,9 @@ interface CheckboxOptionProps {
key: string | number;
}

export function CheckboxOption({
value,
className,
children,
key,
}: CheckboxOptionProps) {
export function CheckboxOption({ children }: CheckboxOptionProps) {
return (
<Option>
<Option value={undefined}>
<Checkbox />
{children}
</Option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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 { CheckboxOption } from "./checkboxOption";

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/checkboxSelect/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/checkboxSelect/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/checkboxSelect/combobox.tsx

View workflow job for this annotation

GitHub Actions / Build & lint

Insert `,`
);

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

useEffect(() => {
const filtered = options.filter((option) =>
option.label.toLowerCase().includes(query.toLowerCase())
);
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-absolute 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-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">
<ChevronDownIcon className="k1-h-6 k1-w-6" />
</Button>
{showClearButton && <ClearButton onClose={handleClear} />}
</div>
<Options anchor="bottom start" className="k1-w-[var(--input-width)]">
{filteredOptions.length === 0 ? (
<div className="k1-px-4 k1-py-2">Žadné vysledky</div>
) : (
filteredOptions.map((option) => (
<CheckboxOption
key={option.id}
value={option.value}
className={twMerge("", className)}
>
{option.label}
</CheckboxOption>
))
)}
</Options>
</ComboboxPrimitive>
);
}
);

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("k1-w-full k1-py-2 k1-pl-3", 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
Expand Up @@ -113,15 +113,15 @@ const Combobox = forwardRef<
multiple
as="div"
ref={ref}
className={twMerge("k1-relative k1-w-full", className)}
className={twMerge("k1-absolute 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"
className="k1-flex-grow k1-peer"
value={
selectedValues.length > 0 ? selectedValues.join(", ") : query
}
Expand All @@ -132,12 +132,12 @@ const Combobox = forwardRef<
}}
/>
{children}
<Button className="k1-flex-shrink-0 k1-h-full k1-flex k1-items-center k1-pr-2">
<Button className="k1-flex-shrink-0 k1-h-full k1-flex k1-items-center">
<ChevronDownIcon className="k1-h-6 k1-w-6" />
</Button>
{showClearButton && <ClearButton onClose={handleClear} />}
</div>
<Options anchor="bottom start">
<Options anchor="bottom start" className="k1-w-[var(--input-width)]">
{filteredOptions.length === 0 ? (
<div className="k1-px-4 k1-py-2">Žadné vysledky</div>
) : (
Expand Down Expand Up @@ -168,10 +168,7 @@ const Input = forwardRef<
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
)}
className={twMerge("k1-w-full k1-py-2 k1-pl-3", className)}
{...props}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/ui/input/combobobx/option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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-flex hover:k1-bg-neutral-backdrop-hover focus:k1-bg-neutral-backdrop-active k1-text-neutral-fg k1-text-base k1-w-full",
"k1-px-4 k1-py-3 k1-border-b k1-border-neutral k1-justify-start k1-items-start k1-flex hover:k1-bg-neutral-backdrop-hover focus:k1-bg-neutral-backdrop-active k1-text-neutral-fg k1-text-base",
className
)}
{...props}
Expand Down
Loading

0 comments on commit c560605

Please sign in to comment.