Initial Commit
This commit is contained in:
206
src/components/drop-down/index.tsx
Normal file
206
src/components/drop-down/index.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import {IChannel} from 'interfaces';
|
||||
import {Label} from 'components/label';
|
||||
|
||||
import {DropdownContainer, DropdownInput, DropdownMenu, DropdownMenuItem} from './style';
|
||||
|
||||
const keys = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'];
|
||||
|
||||
interface IDropdown {
|
||||
itemKey: string;
|
||||
searchTerm?: string;
|
||||
onSelect: (key: string) => void;
|
||||
items: IChannel[];
|
||||
label: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
const maxNumOfItemsShown = 30;
|
||||
|
||||
export const Dropdown = (props: IDropdown): React.JSX.Element => {
|
||||
const {itemKey, searchTerm, onSelect, items, label, name, className} = props;
|
||||
const [showDropdownMenu, setShowDropdownMenu] = useState(false);
|
||||
const [input, setInput] = useState(searchTerm || '');
|
||||
const [filteredItems, setFilteredItems] = useState(items);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const allItemsRef = useRef<any[]>([]);
|
||||
const parentElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollSelectedItemInView = (index: number) => {
|
||||
if (!parentElementRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdownMenu = parentElementRef.current;
|
||||
const menuItems = allItemsRef.current;
|
||||
|
||||
if (!menuItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItem = menuItems[index];
|
||||
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isOutOfUpperView = menuItem.offsetTop < dropdownMenu.scrollTop;
|
||||
const isOutOfLowerView = menuItem.offsetTop + menuItem.clientHeight > dropdownMenu.scrollTop + dropdownMenu.clientHeight;
|
||||
|
||||
if (isOutOfUpperView) {
|
||||
dropdownMenu.scrollTop = menuItem.offsetTop;
|
||||
} else if (isOutOfLowerView) {
|
||||
dropdownMenu.scrollTop = menuItem.offsetTop + menuItem.clientHeight - dropdownMenu.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectInput = (value: string) => {
|
||||
setInput(value);
|
||||
onSelect(value);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!parentElementRef.current || parentElementRef.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filteredItems.length) {
|
||||
setSelectInput('');
|
||||
}
|
||||
|
||||
setShowDropdownMenu(false);
|
||||
onSelect(input);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollSelectedItemInView(selectedIndex);
|
||||
}, [selectedIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const filteredItems = items.filter(item => {
|
||||
if ((item as any)[itemKey].toLowerCase().indexOf(input.toLowerCase()) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
|
||||
setFilteredItems(filteredItems);
|
||||
}, [input, items, itemKey]);
|
||||
|
||||
const selectItemInFocusBy = (offset: number) => {
|
||||
const lastIndex = filteredItems.length - 1;
|
||||
const nextIndex = selectedIndex + offset;
|
||||
|
||||
if (nextIndex > lastIndex) {
|
||||
setSelectedIndex(0);
|
||||
} else if (nextIndex < 0) {
|
||||
setSelectedIndex(lastIndex);
|
||||
} else {
|
||||
setSelectedIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (keys.indexOf(event.key) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [arrDown, arrUp, enter, escape] = keys;
|
||||
const moves = {
|
||||
[arrDown]: 1,
|
||||
[arrUp]: -1
|
||||
};
|
||||
const move = moves[event.key];
|
||||
|
||||
if (move !== undefined) {
|
||||
event.preventDefault();
|
||||
selectItemInFocusBy(move);
|
||||
}
|
||||
|
||||
if (event.key === enter) {
|
||||
if (filteredItems[selectedIndex]) {
|
||||
setSelectInput((filteredItems[selectedIndex] as any)[itemKey]);
|
||||
setShowDropdownMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === escape) {
|
||||
event.preventDefault();
|
||||
setShowDropdownMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
setSelectInput((filteredItems[index] as any)[itemKey]);
|
||||
setSelectedIndex(index);
|
||||
setShowDropdownMenu(false);
|
||||
};
|
||||
|
||||
const generateMenuOptions = () => {
|
||||
return filteredItems.length ? (
|
||||
filteredItems.slice(0, maxNumOfItemsShown).map((item, index) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
ref={ref => {
|
||||
allItemsRef.current[index] = ref;
|
||||
}}
|
||||
selected={selectedIndex === index}
|
||||
key={`dropdown-menu-item-${index}`}
|
||||
onClick={() => selectItem(index)}>
|
||||
{(item as any)[itemKey]}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem disabled={true}>No results found</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {target} = event;
|
||||
setInput(target.value);
|
||||
|
||||
if (!showDropdownMenu) {
|
||||
setShowDropdownMenu(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDropdownMenu = () => setShowDropdownMenu(!showDropdownMenu);
|
||||
|
||||
return (
|
||||
<DropdownContainer>
|
||||
<Label htmlFor="autocomplete" text={label} />
|
||||
<DropdownInput
|
||||
onKeyDown={handleOnKeyDown}
|
||||
showMenu={showDropdownMenu}
|
||||
autoComplete="off"
|
||||
name={name}
|
||||
onChange={handleInput}
|
||||
value={input}
|
||||
onClick={toggleDropdownMenu}
|
||||
className={className}
|
||||
/>
|
||||
{showDropdownMenu && (
|
||||
<DropdownMenu ref={parentElementRef} className="testId-generatedMenuOptions">
|
||||
{generateMenuOptions()}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</DropdownContainer>
|
||||
);
|
||||
};
|
||||
61
src/components/drop-down/style.ts
Normal file
61
src/components/drop-down/style.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Input from 'components/forms/Input';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {spacing, colors, typography, primaryBorderRadius} = Theme;
|
||||
|
||||
export const DropdownContainer = styled.default.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin: ${spacing.xSmall} 0;
|
||||
`;
|
||||
|
||||
export const DropdownInput = styled.default(Input)<{showMenu?: boolean}>`
|
||||
${({showMenu}) =>
|
||||
showMenu &&
|
||||
styled.css`
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-width: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const DropdownMenu = styled.default.div`
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
top: calc(100% - 4px);
|
||||
position: absolute;
|
||||
height: auto;
|
||||
max-height: 210px;
|
||||
border: 1px solid ${colors.gray400};
|
||||
border-top-width: 0;
|
||||
z-index: 4;
|
||||
background-color: ${colors.white};
|
||||
border-bottom-left-radius: ${primaryBorderRadius};
|
||||
border-bottom-right-radius: ${primaryBorderRadius};
|
||||
`;
|
||||
|
||||
export const DropdownMenuItem = styled.default.div<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
}>`
|
||||
line-height: ${spacing.large};
|
||||
padding: ${spacing.xsmall} ${spacing.medium};
|
||||
font-size: ${typography.fontSizeS};
|
||||
word-wrap: break-word;
|
||||
${({active, selected, disabled}) => styled.css`
|
||||
background-color: ${active || selected ? colors.gray300 : 'transparent'};
|
||||
font-weight: ${selected ? 'bold' : 'normal'};
|
||||
color: ${disabled ? colors.gray500 : colors.gray900}
|
||||
|
||||
:hover{
|
||||
background-color: ${disabled ? colors.white : colors.gray300};
|
||||
cursor: ${disabled ? 'text' : 'pointer'};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
Reference in New Issue
Block a user