diff --git a/apps/design-system/stories/multipleOption.stories.tsx b/apps/design-system/stories/multipleOption.stories.tsx deleted file mode 100644 index c2575b5..0000000 --- a/apps/design-system/stories/multipleOption.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { MultipleOption } from "../../../packages/design-system/src/ui/input/combobobx/multipleOption"; - -const meta: Meta = { - title: "Components/MultipleOption", - component: MultipleOption, - argTypes: {}, - tags: ["autodocs"], -}; - -type MultipleOptionStory = StoryObj; - -export const Default: MultipleOptionStory = { - args: {}, -}; - -export default meta; diff --git a/apps/design-system/stories/multipleOptionSelect.stories.tsx b/apps/design-system/stories/multipleOptionSelect.stories.tsx new file mode 100644 index 0000000..9d59a71 --- /dev/null +++ b/apps/design-system/stories/multipleOptionSelect.stories.tsx @@ -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 = { + title: "Components/MultipleSelectField", + component: MultipleSelectField, + argTypes: {}, + tags: ["autodocs"], +}; + +type MultipleSelectFieldStory = StoryObj; + +export const Default: MultipleSelectFieldStory = { + args: {}, +}; + +export default meta; diff --git a/apps/design-system/stories/select-input.stories.tsx b/apps/design-system/stories/select-input.stories.tsx index 2bf0c2b..8d39589 100644 --- a/apps/design-system/stories/select-input.stories.tsx +++ b/apps/design-system/stories/select-input.stories.tsx @@ -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 = { - title: "Components/SelectInput", - component: SelectInputField, +const meta: Meta = { + title: "Components/SingleSelectField", + component: SingleSelectField, argTypes: { label: { control: "text" }, error: { control: "text" }, @@ -26,9 +26,9 @@ const meta: Meta = { tags: ["autodocs"], }; -type SelectInputStory = StoryObj; +type SingleSelectFieldStory = StoryObj; -export const Default: SelectInputStory = { +export const Default: SingleSelectFieldStory = { args: {}, }; diff --git a/packages/design-system/.eslintrc.js b/packages/design-system/.eslintrc.js index 26795e4..33c8f55 100644 --- a/packages/design-system/.eslintrc.js +++ b/packages/design-system/.eslintrc.js @@ -10,4 +10,12 @@ module.exports = { env: { node: true, }, + rules: { + "prettier/prettier": [ + "error", + { + endOfLine: "auto", + }, + ], + }, }; diff --git a/packages/design-system/package.json b/packages/design-system/package.json index f69067a..5c36360 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -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", diff --git a/packages/design-system/src/ui/input/combobobx/multipleOption.tsx b/packages/design-system/src/ui/input/combobobx/multipleOption.tsx index 07a5adb..342719e 100644 --- a/packages/design-system/src/ui/input/combobobx/multipleOption.tsx +++ b/packages/design-system/src/ui/input/combobobx/multipleOption.tsx @@ -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"; diff --git a/packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx b/packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx new file mode 100644 index 0000000..8fae1d6 --- /dev/null +++ b/packages/design-system/src/ui/input/combobobx/multipleSelect/combobox.tsx @@ -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 { + showClearButton?: boolean; + onClear?: () => void; + defaultValue?: string[]; + onInputChange?: (value: string[]) => void; + options: Array<{ id: string | number; value: string; label: string }>; + showPlaceholder?: boolean; + children?: React.ReactNode; +} + +const Combobox = forwardRef< + React.ElementRef, + ComboboxProps +>( + ( + { + className, + children, + showClearButton, + onChange, + defaultValue, + onClear, + onInputChange, + options, + ...props + }, + ref + ) => { + //Use state for selecting values. If nothing is provided, defaultValue will become the selected value + const [selectedValues, setSelectedValues] = useState( + defaultValue instanceof Array + ? defaultValue + : defaultValue + ? [defaultValue] + : [] + ); + + const [query, setQuery] = useState( + 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) => { + const value = event.target.value; + setQuery(value); + if (onInputChange) { + onInputChange([value]); + } + }; + + return ( + +
+ 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} + + {showClearButton && } +
+ + {filteredOptions.length === 0 ? ( +
Žadné vysledky
+ ) : ( + filteredOptions.map((option) => ( + + )) + )} +
+
+ ); + } +); + +const Input = forwardRef< + React.ElementRef, + InputPrimitiveProps & { className?: string } // InputProps has more variable className, but we need string +>(({ className, ...props }, ref) => { + const inputRef = useRef(null); + + useImperativeHandle(ref, () => inputRef.current as HTMLInputElement); + + return ( + + ); +}); +Input.displayName = InputPrimitive.displayName; + +const Options = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +Options.displayName = "Combobox.Options"; + +const Button = forwardRef< + React.ElementRef, + ButtonProps & { className?: string } +>(({ className, ...props }, ref) => ( + +)); +Button.displayName = "Combobox.Button"; + +export { Combobox, Input, Options, Button }; diff --git a/packages/design-system/src/ui/input/combobobx/multipleSelect/multipleSelectField.tsx b/packages/design-system/src/ui/input/combobobx/multipleSelect/multipleSelectField.tsx new file mode 100644 index 0000000..9a42852 --- /dev/null +++ b/packages/design-system/src/ui/input/combobobx/multipleSelect/multipleSelectField.tsx @@ -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, + "className" | "style" +>; + +type Props = { + label?: string; + error?: string; + showClearButton?: boolean; + icon?: React.ComponentType>; +} & 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, 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 ( + + {hasIcon && } + console.log("Selected value:", value)} + onInputChange={(value) => console.log("Input value:", value)} + onClear={() => console.log("Cleared")} + showPlaceholder={showPlaceholder} + > + {label && ( + + )} + + {hasError && {error}} + + ); + } +); + +export { MultipleSelectField }; diff --git a/packages/design-system/src/ui/input/combobobx/singleOption.tsx b/packages/design-system/src/ui/input/combobobx/option.tsx similarity index 96% rename from packages/design-system/src/ui/input/combobobx/singleOption.tsx rename to packages/design-system/src/ui/input/combobobx/option.tsx index 293cb58..d8c4642 100644 --- a/packages/design-system/src/ui/input/combobobx/singleOption.tsx +++ b/packages/design-system/src/ui/input/combobobx/option.tsx @@ -9,7 +9,7 @@ const Option = forwardRef< { @@ -104,6 +104,7 @@ const Combobox = forwardRef< return ( , Props>( +const SingleSelectField = forwardRef, Props>( ({ label, error, showClearButton, icon }: Props, ref) => { // if error is present, we pass it to all the sub-components const hasError = !!error; @@ -71,4 +71,4 @@ const SelectInputField = forwardRef, Props>( } ); -export { SelectInputField }; +export { SingleSelectField };