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

[FEQ]Yaswanth/FEQ-1440/Added Dropdown component #27

145 changes: 145 additions & 0 deletions lib/components/Dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
.deriv-dropdown {
width: 100%;
position: relative;
cursor: pointer;

&--disabled {
pointer-events: none;

& label {
color: var(--system-light-5-active-background, #999);
}
}

&__button {
all: unset;
right: 1.6rem;
transform: rotate(0);
transform-origin: 50% 45%;
transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);

&--active {
transform: rotate(180deg);
}
}
&__content {
width: 100%;
background: var(--system-light-8-primary-background, #fff);
display: flex;
align-items: center;

.deriv-textfield__field {
cursor: pointer;
}
}

&__field {
position: absolute;
inset: 0;
min-width: 0; /* this is required to reset input's default width */
padding-left: 2rem;
display: flex;
flex-grow: 1;
font-family: inherit;
outline: 0;
font-size: 1.4rem;
background-color: transparent;
color: var(--system-light-2-general-text, #333);
transition: border-color 0.2s;
cursor: unset;
user-select: none;
&::selection {
background-color: transparent;
}

&::placeholder {
color: transparent;
}
}

&__field:placeholder-shown ~ &__label {
font-size: 1.4rem;
cursor: text;
top: 30%;
padding: 0;
}

&__field:placeholder-shown ~ &__label--with-icon {
left: 4.4rem;
}

label,
&__field:focus ~ &__label {
display: inline-block;
position: absolute;
top: 0;
bottom: 0;
left: 16px;
display: flex;
align-items: center;
pointer-events: none;
text-transform: capitalize;
transition: all 0.15s ease-out;
padding: 0;
}

&__field:focus ~ &__label {
color: var(--brand-blue, #85acb0);
}

&__items {
position: absolute;
top: 2.7rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
z-index: 2;
border-radius: 0.4rem;
background: var(--system-light-8-primary-background, #fff);
box-shadow: 0 3.2rem 6.4rem 0 rgba(14, 14, 14, 0.14);
overflow-y: auto;

& > :first-child {
border-radius: 0.4rem 0.4rem 0 0;
}

& > :last-child {
border-radius: 0 0 0.4rem 0.4rem;
}

&--sm {
max-height: 22rem;
}

&--md {
max-height: 42rem;
}

&--lg {
max-height: 66rem;
}
}

&__icon {
position: absolute;
left: 1.6rem;
width: 1.6rem;
height: 1.6rem;
}

&__item {
padding: 10px 16px;
width: 100%;
z-index: 2;

&:hover:not(&--active) {
cursor: pointer;
background: var(--system-light-6-hover-background, #e6e9e9);
}

&--active {
background: var(--system-light-5-active-background, #d6dadb);
}
}
}
158 changes: 158 additions & 0 deletions lib/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { isValidElement, useCallback, useEffect, useState } from 'react';
import clsx from 'clsx';
import { useCombobox } from 'downshift';
import { TGenericSizes } from "../../types";
import { Text } from '../Text';
import {Input } from '../Input/index';
import './Dropdown.scss';

type InputProps = React.ComponentProps<typeof Input>;
type TProps = {
disabled?: boolean;
dropdownIcon: React.ReactNode;
errorMessage?: InputProps['message'];
icon?: React.ReactNode;
isRequired?: boolean;
label?: InputProps['label'];
list: {
text?: React.ReactNode;
value?: string;
}[];
listHeight?: Extract<TGenericSizes, 'lg' | 'md' | 'sm'>;
name: InputProps['name'];
onChange?: (inputValue: string) => void;
onSelect: (value: string) => void;
value?: InputProps['value'];
variant?: 'comboBox' | 'prompt';
};

export const Dropdown = ({
disabled,
dropdownIcon,
errorMessage,
icon = false,
label,
list,
listHeight = 'md',
name,
onChange,
onSelect,
value,
variant = 'prompt',
}:TProps) => {
const [items, setItems] = useState(list);
const [shouldFilterList, setShouldFilterList] = useState(false);
const clearFilter = useCallback(() => {
setShouldFilterList(false);
setItems(list);
}, [list]);
const reactNodeToString = function (reactNode: React.ReactNode): string {
let string = '';
if (typeof reactNode === 'string') {
string = reactNode;
} else if (typeof reactNode === 'number') {
string = reactNode.toString();
} else if (reactNode instanceof Array) {
reactNode.forEach(function (child) {
string += reactNodeToString(child);
});
} else if (isValidElement(reactNode)) {
string += reactNodeToString(reactNode.props.children);
}
return string;
};
const { closeMenu, getInputProps, getItemProps, getMenuProps, getToggleButtonProps, isOpen, openMenu } =
useCombobox({
defaultSelectedItem: items.find(item => item.value === value) ?? null,
items,
itemToString(item) {
return item ? reactNodeToString(item.text) : '';
},
onInputValueChange({ inputValue }) {
onChange?.(inputValue ?? '');
if (shouldFilterList) {
setItems(
list.filter(item =>
reactNodeToString(item.text)
.toLowerCase()
.includes(inputValue?.toLowerCase() ?? '')
)
);
}
},
onIsOpenChange({ isOpen }) {
if (!isOpen) {
clearFilter();
}
},
onSelectedItemChange({ selectedItem }) {
onSelect(selectedItem?.value ?? '');
closeMenu();
},
});

const handleInputClick = useCallback(() => {
variant === 'comboBox' && setShouldFilterList(true);

if (isOpen) {
closeMenu();
} else {
openMenu();
}
}, [closeMenu, isOpen, openMenu, variant]);

useEffect(() => {
setItems(list);
}, [list]);

return (
<div
className={clsx('deriv-dropdown', {
'deriv-dropdown--disabled': disabled,
})}
{...getToggleButtonProps()}
>
<div className='deriv-dropdown__content'>
<Input
disabled={disabled}
message={errorMessage}
label={reactNodeToString(label)}
name={name}
onClickCapture={handleInputClick}
onKeyUp={() => setShouldFilterList(true)}
readOnly={variant !== 'comboBox'}
leftPlaceholder={icon ? icon : undefined}
rightPlaceholder={ (
<button
className={clsx('deriv-dropdown__button', {
'deriv-dropdown__button--active': isOpen,
})}
>
{dropdownIcon}
</button>
)}
type='text'
value={value}
{...getInputProps()}
/>
</div>
<ul className={`deriv-dropdown__items deriv-dropdown__items--${listHeight}`} {...getMenuProps()}>
{isOpen &&
items.map((item, index) => (
<li
className={clsx('deriv-dropdown__item', {
'deriv-dropdown__item--active': value === item.value,
})}
key={item.value}
onClick={() => clearFilter()}
{...getItemProps({ index, item })}
>
<Text size='sm' weight={value === item.value ? 'bold' : 'normal'}>
{item.text}
</Text>
</li>
))}
</ul>
</div>
);
};
7 changes: 6 additions & 1 deletion lib/components/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import HelperMessage from "./HelperMessage";
import "./Input.scss";

export type InputVariants = "general" | "success" | "error";
interface InputProps
interface InputProps
extends Omit<ComponentProps<"input">, "style" | "placeholder"> {
label?: string;
leftPlaceholder?: ReactNode;
rightPlaceholder?: ReactNode;
error?: boolean;
variant?: InputVariants;
Expand All @@ -30,6 +31,7 @@ export const Input = ({
id,
error,
message,
leftPlaceholder,
rightPlaceholder,
variant = "general",
className,
Expand All @@ -38,6 +40,9 @@ export const Input = ({
}: InputProps) => {
return (
<div className="deriv-input">
{leftPlaceholder && (
<div className="deriv-input--left-content">{leftPlaceholder}</div>
)}
<input
placeholder={label}
className={clsx(
Expand Down
1 change: 0 additions & 1 deletion lib/components/Text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,3 @@ export const Text = ({

return <Tag className={textClassNames}>{children}</Tag>;
};

29 changes: 29 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 @@ -51,6 +51,7 @@
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"clsx": "^2.1.0",
"downshift": "^8.3.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
Expand Down
Loading