Skip to content

Commit

Permalink
Added navigation with arrow keys and selection with enter
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-dalby committed Oct 29, 2024
1 parent da39567 commit 456836e
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 25 deletions.
107 changes: 86 additions & 21 deletions client/src/components/common/BaseDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,40 @@ const BaseDropdown: React.FC<BaseDropdownProps> = ({
maxLength
}) => {
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);

const getAllItems = (sections: Section[]): string[] => {
return sections.reduce((acc: string[], section) => [...acc, ...section.items], []);
};

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setHighlightedIndex(-1);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

useEffect(() => {
setHighlightedIndex(-1);
}, [isOpen, value]);

useEffect(() => {
if (isOpen && highlightedIndex >= 0 && listRef.current) {
const highlightedElement = listRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
if (highlightedElement) {
highlightedElement.scrollIntoView({ block: 'nearest' });
}
}
}, [highlightedIndex, isOpen]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = maxLength ? e.target.value.slice(0, maxLength) : e.target.value;
onChange(newValue);
Expand All @@ -53,6 +73,7 @@ const BaseDropdown: React.FC<BaseDropdownProps> = ({
const handleOptionClick = (option: string) => {
onSelect(option);
setIsOpen(false);
setHighlightedIndex(-1);
};

const handleKeyDown = (e: React.KeyboardEvent) => {
Expand All @@ -61,21 +82,55 @@ const BaseDropdown: React.FC<BaseDropdownProps> = ({
if (e.defaultPrevented) return;
}

if (e.key === 'Enter') {
e.preventDefault();
const sections = getSections(value);
if (sections.length > 0) {
const lastSection = sections[sections.length - 1];
if (lastSection.items.length > 0) {
handleOptionClick(lastSection.items[0]);
const sections = getSections(value);
const allItems = getAllItems(sections);
const totalItems = allItems.length;

switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
}
}
} else if (e.key === 'Escape') {
setIsOpen(false);
setHighlightedIndex(prev =>
prev < totalItems - 1 ? prev + 1 : totalItems - 1
);
break;

case 'ArrowUp':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
}
setHighlightedIndex(prev => prev > 0 ? prev - 1 : 0);
break;

case 'Enter':
e.preventDefault();
if (isOpen && highlightedIndex >= 0 && highlightedIndex < totalItems) {
handleOptionClick(allItems[highlightedIndex]);
} else if (sections.length > 0) {
const lastSection = sections[sections.length - 1];
if (lastSection.items.length > 0) {
handleOptionClick(lastSection.items[0]);
}
}
break;

case 'Escape':
setIsOpen(false);
setHighlightedIndex(-1);
break;

case 'Tab':
setIsOpen(false);
setHighlightedIndex(-1);
break;
}
};

const sections = getSections(value);
let currentIndex = -1;

return (
<div className="relative" ref={dropdownRef}>
Expand All @@ -98,7 +153,10 @@ const BaseDropdown: React.FC<BaseDropdownProps> = ({
/>
</div>
{isOpen && sections.length > 0 && (
<ul className="absolute z-10 mt-1 w-full bg-gray-700 border border-gray-600 rounded-md shadow-lg max-h-60 overflow-auto">
<ul
ref={listRef}
className="absolute z-10 mt-1 w-full bg-gray-700 border border-gray-600 rounded-md shadow-lg max-h-60 overflow-auto"
>
{sections.map((section, sectionIndex) => (
<React.Fragment key={section.title}>
{sectionIndex > 0 && (
Expand All @@ -107,16 +165,23 @@ const BaseDropdown: React.FC<BaseDropdownProps> = ({
<li className="px-3 py-1 text-xs font-medium text-gray-400 bg-gray-800">
{section.title}
</li>
{section.items.map((item, index) => (
<li
key={`${section.title}-${index}`}
className="px-4 py-2 hover:bg-gray-600 cursor-pointer text-white text-sm"
onClick={() => handleOptionClick(item)}
onMouseDown={(e) => e.preventDefault()}
>
{item}
</li>
))}
{section.items.map((item) => {
currentIndex++;
return (
<li
key={`${section.title}-${item}`}
className={`px-4 py-2 cursor-pointer text-white text-sm ${
highlightedIndex === currentIndex ? 'bg-gray-600' : 'hover:bg-gray-600'
}`}
onClick={() => handleOptionClick(item)}
onMouseDown={(e) => e.preventDefault()}
onMouseEnter={() => setHighlightedIndex(currentIndex)}
data-index={currentIndex}
>
{item}
</li>
);
})}
</React.Fragment>
))}
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const CategorySuggestions: React.FC<CategorySuggestionsProps> = ({
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ',') {
if (e.key === ',') {
e.preventDefault();
const term = handleHashtag
? internalValue.slice(internalValue.lastIndexOf('#') + 1).trim()
Expand All @@ -80,9 +80,7 @@ const CategorySuggestions: React.FC<CategorySuggestionsProps> = ({
const handleSelect = (option: string) => {
let newCategory;
if (option.startsWith('Add new:')) {
newCategory = handleHashtag
? internalValue.slice(internalValue.lastIndexOf('#') + 1).trim()
: internalValue.trim();
newCategory = option.slice(9).trim();
} else {
newCategory = option;
}
Expand Down

0 comments on commit 456836e

Please sign in to comment.