Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-1201] dev: dropdowns #4721

Merged
merged 6 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/ui/src/dropdown/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Below is a detailed list of the props included:

### Root Props

Check notice on line 3 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L3

Expected: 1; Actual: 0; Below
- value: string | string[]; - Current selected value.

Check notice on line 4 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L4

Lists should be surrounded by blank lines
- onChange: (value: string | string []) => void; - Callback function for handling value changes.
- options: TDropdownOption[] | undefined; - Array of options.
- onOpen?: () => void; - Callback function triggered when the dropdown opens.
- onClose?: () => void; - Callback function triggered when the dropdown closes.
- containerClassName?: (isOpen: boolean) => string; - Function to return the class name for the container based on the open state.
- tabIndex?: number; - Sets the tab index for the dropdown.
- placement?: Placement; - Determines the placement of the dropdown (e.g., top, bottom, left, right).
- disabled?: boolean; - Disables the dropdown if set to true.

---

### Button Props

Check notice on line 16 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L16

Expected: 1; Actual: 0; Below
- buttonContent?: (isOpen: boolean) => React.ReactNode; - Function to render the content of the button based on the open state.

Check notice on line 17 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L17

Lists should be surrounded by blank lines
- buttonContainerClassName?: string; - Class name for the button container.
- buttonClassName?: string; - Class name for the button itself.

---

### Input Props

Check notice on line 23 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L23

Expected: 1; Actual: 0; Below
- disableSearch?: boolean; - Disables the search input if set to true.

Check notice on line 24 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L24

Lists should be surrounded by blank lines
- inputPlaceholder?: string; - Placeholder text for the search input.
- inputClassName?: string; - Class name for the search input.
- inputIcon?: React.ReactNode; - Icon to be displayed in the search input.
- inputContainerClassName?: string; - Class name for the search input container.

---

### Options Props

Check notice on line 32 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L32

Expected: 1; Actual: 0; Below
- keyExtractor: (option: TDropdownOption) => string; - Function to extract the key from each option.

Check notice on line 33 in packages/ui/src/dropdown/Readme.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/ui/src/dropdown/Readme.md#L33

Lists should be surrounded by blank lines
- optionsContainerClassName?: string; - Class name for the options container.
- queryArray: string[]; - Array of strings to be used for querying the options.
- sortByKey: string; - Key to sort the options by.
- firstItem?: (optionValue: string) => boolean; - Function to determine if an option should be the first item.
- renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode; - Function to render each option.
- loader?: React.ReactNode; - Loader element to be displayed while options are being loaded.
- disableSorting?: boolean; - Disables sorting of the options if set to true.

---

These properties offer extensive control over the dropdown's behavior and presentation, making it a highly versatile component suitable for various scenarios.
38 changes: 38 additions & 0 deletions packages/ui/src/dropdown/common/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Fragment } from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// helper
import { cn } from "../../../helpers";
import { IMultiSelectDropdownButton, ISingleSelectDropdownButton } from "../dropdown";

export const DropdownButton: React.FC<IMultiSelectDropdownButton | ISingleSelectDropdownButton> = (props) => {
const {
isOpen,
buttonContent,
buttonClassName,
buttonContainerClassName,
handleOnClick,
value,
setReferenceElement,
disabled,
} = props;
return (
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
{buttonContent ? <>{buttonContent(isOpen)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
</button>
</Combobox.Button>
);
};
4 changes: 4 additions & 0 deletions packages/ui/src/dropdown/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./input-search";
export * from "./button";
export * from "./options";
export * from "./loader";
58 changes: 58 additions & 0 deletions packages/ui/src/dropdown/common/input-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { FC, useEffect, useRef } from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// icons
import { Search } from "lucide-react";
// helpers
import { cn } from "../../../helpers";

interface IInputSearch {
isOpen: boolean;
query: string;
updateQuery: (query: string) => void;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;
inputClassName?: string;
inputPlaceholder?: string;
}

export const InputSearch: FC<IInputSearch> = (props) => {
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder } = props;

const inputRef = useRef<HTMLInputElement | null>(null);

const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
updateQuery("");
}
};

useEffect(() => {
if (isOpen) {
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
return (
<div
className={cn(
"flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2",
inputContainerClassName
)}
>
{inputIcon ? <>{inputIcon}</> : <Search className="h-4 w-4 text-custom-text-300" aria-hidden="true" />}
<Combobox.Input
as="input"
ref={inputRef}
className={cn(
"w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none",
inputClassName
)}
value={query}
onChange={(e) => updateQuery(e.target.value)}
placeholder={inputPlaceholder ?? "Search"}
onKeyDown={searchInputKeyDown}
/>
</div>
);
};
9 changes: 9 additions & 0 deletions packages/ui/src/dropdown/common/loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";

export const DropdownOptionsLoader = () => (
<div className="flex flex-col gap-1 animate-pulse">
{Array.from({ length: 6 }, (_, i) => (
<div key={i} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
))}
</div>
);
88 changes: 88 additions & 0 deletions packages/ui/src/dropdown/common/options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// icons
import { Check } from "lucide-react";
// components
import { DropdownOptionsLoader, InputSearch } from ".";
// helpers
import { cn } from "../../../helpers";
// types
import { IMultiSelectDropdownOptions, ISingleSelectDropdownOptions } from "../dropdown";

export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSelectDropdownOptions> = (props) => {
const {
isOpen,
query,
setQuery,
inputIcon,
inputPlaceholder,
inputClassName,
inputContainerClassName,
disableSearch,
keyExtractor,
options,
value,
renderItem,
loader,
} = props;
return (
<>
{!disableSearch && (
<InputSearch
isOpen={isOpen}
query={query}
updateQuery={(query) => setQuery(query)}
inputIcon={inputIcon}
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
/>
)}
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<>
{options ? (
options.length > 0 ? (
options?.map((option) => (
<Combobox.Option
key={keyExtractor(option)}
value={option.data[option.value]}
className={({ active, selected }) =>
cn(
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
},
option.className && option.className({ active, selected })
)
}
>
{({ selected }) => (
<>
{renderItem ? (
<>{renderItem({ value: option.data[option.value], selected })}</>
) : (
<>
<span className="flex-grow truncate">{value}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 italic text-custom-text-400">No matching results</p>
)
) : loader ? (
<> {loader} </>
) : (
<DropdownOptionsLoader />
)}
</>
</div>
</>
);
};
94 changes: 94 additions & 0 deletions packages/ui/src/dropdown/dropdown.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Placement } from "@popperjs/core";

export interface IDropdown {
// root props
onOpen?: () => void;
onClose?: () => void;
containerClassName?: (isOpen: boolean) => string;
tabIndex?: number;
placement?: Placement;
disabled?: boolean;

// button props
buttonContent?: (isOpen: boolean) => React.ReactNode;
buttonContainerClassName?: string;
buttonClassName?: string;

// input props
disableSearch?: boolean;
inputPlaceholder?: string;
inputClassName?: string;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;

// options props
keyExtractor: (option: TDropdownOption) => string;
optionsContainerClassName?: string;
queryArray: string[];
sortByKey: string;
firstItem?: (optionValue: string) => boolean;
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
loader?: React.ReactNode;
disableSorting?: boolean;
}

export interface TDropdownOption {
data: any;
value: string;
className?: ({ active, selected }: { active: boolean; selected: boolean }) => string;
}

export interface IMultiSelectDropdown extends IDropdown {
value: string[];
onChange: (value: string[]) => void;
options: TDropdownOption[] | undefined;
}

export interface ISingleSelectDropdown extends IDropdown {
value: string;
onChange: (value: string) => void;
options: TDropdownOption[] | undefined;
}

export interface IDropdownButton {
isOpen: boolean;
buttonContent?: (isOpen: boolean) => React.ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
handleOnClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
setReferenceElement: (element: HTMLButtonElement | null) => void;
disabled?: boolean;
}

export interface IMultiSelectDropdownButton extends IDropdownButton {
value: string[];
}

export interface ISingleSelectDropdownButton extends IDropdownButton {
value: string;
}

export interface IDropdownOptions {
isOpen: boolean;
query: string;
setQuery: (query: string) => void;

inputPlaceholder?: string;
inputClassName?: string;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;
disableSearch?: boolean;

keyExtractor: (option: TDropdownOption) => string;
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
options: TDropdownOption[] | undefined;
loader?: React.ReactNode;
}

export interface IMultiSelectDropdownOptions extends IDropdownOptions {
value: string[];
}

export interface ISingleSelectDropdownOptions extends IDropdownOptions {
value: string;
}
3 changes: 3 additions & 0 deletions packages/ui/src/dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./common";
export * from "./multi-select";
export * from "./single-select";
Loading
Loading