mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-27 21:54:15 +00:00
* Initial plan * Refactor SearchableDropdown with Fluent UI components and add tests - Replace native HTML elements with Fluent UI components (Stack, DefaultButton, Text) - Extract inline styles to SearchableDropdown.styles.ts - Add comprehensive unit tests (14 test cases) - Verify behavior consistency with AccountSwitcher tests Co-authored-by: nishthaAhujaa <45535788+nishthaAhujaa@users.noreply.github.com> * Optimize SearchableDropdown with useMemo for filteredItems Co-authored-by: nishthaAhujaa <45535788+nishthaAhujaa@users.noreply.github.com> * Fix text alignment to match original UI - ensure left alignment - Add flexContainer.justifyContent: "flex-start" to button styles - Add textAlign: "left" to button label, item styles, and empty message - Restore original left-aligned appearance for placeholder and selected text Co-authored-by: nishthaAhujaa <45535788+nishthaAhujaa@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nishthaAhujaa <45535788+nishthaAhujaa@users.noreply.github.com>
156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
import {
|
|
Callout,
|
|
DefaultButton,
|
|
DirectionalHint,
|
|
ISearchBoxStyles,
|
|
Label,
|
|
SearchBox,
|
|
Stack,
|
|
Text,
|
|
} from "@fluentui/react";
|
|
import * as React from "react";
|
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
import {
|
|
buttonLabelStyles,
|
|
calloutContentStyles,
|
|
chevronStyles,
|
|
emptyMessageStyles,
|
|
getDropdownButtonStyles,
|
|
getItemStyles,
|
|
listContainerStyles,
|
|
} from "./SearchableDropdown.styles";
|
|
|
|
interface SearchableDropdownProps<T> {
|
|
label: string;
|
|
items: T[];
|
|
selectedItem: T | null;
|
|
onSelect: (item: T) => void;
|
|
getKey: (item: T) => string;
|
|
getDisplayText: (item: T) => string;
|
|
placeholder?: string;
|
|
filterPlaceholder?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
onDismiss?: () => void;
|
|
searchBoxStyles?: Partial<ISearchBoxStyles>;
|
|
}
|
|
|
|
export const SearchableDropdown = <T,>({
|
|
label,
|
|
items,
|
|
selectedItem,
|
|
onSelect,
|
|
getKey,
|
|
getDisplayText,
|
|
placeholder = "Select an item",
|
|
filterPlaceholder = "Filter items",
|
|
className,
|
|
disabled = false,
|
|
onDismiss,
|
|
searchBoxStyles: customSearchBoxStyles,
|
|
}: SearchableDropdownProps<T>): React.ReactElement => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [filterText, setFilterText] = useState("");
|
|
const buttonRef = useRef<HTMLDivElement>(null);
|
|
|
|
const closeDropdown = useCallback(() => {
|
|
setIsOpen(false);
|
|
setFilterText("");
|
|
}, []);
|
|
|
|
const filteredItems = useMemo(
|
|
() => items?.filter((item) => getDisplayText(item).toLowerCase().includes(filterText.toLowerCase())),
|
|
[items, filterText, getDisplayText],
|
|
);
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
closeDropdown();
|
|
onDismiss?.();
|
|
}, [closeDropdown, onDismiss]);
|
|
|
|
const handleButtonClick = useCallback(() => {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
setIsOpen(!isOpen);
|
|
}, [isOpen, disabled]);
|
|
|
|
const handleSelect = useCallback(
|
|
(item: T) => {
|
|
onSelect(item);
|
|
closeDropdown();
|
|
},
|
|
[onSelect, closeDropdown],
|
|
);
|
|
|
|
const buttonLabel = selectedItem
|
|
? getDisplayText(selectedItem)
|
|
: items?.length === 0
|
|
? `No ${label}s Found`
|
|
: placeholder;
|
|
|
|
const buttonId = `${className}-button`;
|
|
const buttonStyles = getDropdownButtonStyles(disabled);
|
|
|
|
return (
|
|
<Stack>
|
|
<Label htmlFor={buttonId}>{label}</Label>
|
|
<div ref={buttonRef}>
|
|
<DefaultButton
|
|
id={buttonId}
|
|
className={className}
|
|
onClick={handleButtonClick}
|
|
styles={buttonStyles}
|
|
disabled={disabled}
|
|
>
|
|
<Text styles={buttonLabelStyles}>{buttonLabel}</Text>
|
|
<span style={chevronStyles}>▼</span>
|
|
</DefaultButton>
|
|
</div>
|
|
{isOpen && (
|
|
<Callout
|
|
target={buttonRef.current}
|
|
onDismiss={handleDismiss}
|
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
isBeakVisible={false}
|
|
gapSpace={0}
|
|
setInitialFocus
|
|
>
|
|
<Stack styles={calloutContentStyles} style={{ width: buttonRef.current?.offsetWidth || 300 }}>
|
|
<SearchBox
|
|
placeholder={filterPlaceholder}
|
|
value={filterText}
|
|
onChange={(_, newValue) => setFilterText(newValue || "")}
|
|
styles={customSearchBoxStyles}
|
|
/>
|
|
<Stack styles={listContainerStyles}>
|
|
{filteredItems && filteredItems.length > 0 ? (
|
|
filteredItems.map((item) => {
|
|
const key = getKey(item);
|
|
const isSelected = selectedItem ? getKey(selectedItem) === key : false;
|
|
return (
|
|
<div
|
|
key={key}
|
|
onClick={() => handleSelect(item)}
|
|
style={getItemStyles(isSelected)}
|
|
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f3f2f1")}
|
|
onMouseLeave={(e) =>
|
|
(e.currentTarget.style.backgroundColor = isSelected ? "#e6e6e6" : "transparent")
|
|
}
|
|
>
|
|
<Text>{getDisplayText(item)}</Text>
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<Text styles={emptyMessageStyles}>No items found</Text>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
</Callout>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|