Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Scroll through list of results with keyboard #4294

Merged
merged 4 commits into from
Jun 22, 2022
Merged
Changes from 2 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
48 changes: 38 additions & 10 deletions packages/app/src/components/combo-box/combo-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,52 @@ type TProps<Option extends TOption> = {
* />
* ```
*/
export function ComboBox<Option extends TOption>(props: TProps<Option>) {
export const ComboBox = <Option extends TOption>(props: TProps<Option>) => {
const { options, placeholder, sorter, selectedOption } = props;

const { commonTexts } = useIntl();

const router = useRouter();
const { code } = router.query;
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLUListElement>(null);
const [inputValue, setInputValue] = useState<string>('');
const results = useSearchedOptions<Option>(inputValue, options, sorter);
const breakpoints = useBreakpoints();
const isLargeScreen = breakpoints.md;
const hasRegionSelected = !!code;

function handleInputChange(event: React.ChangeEvent<HTMLInputElement>): void {
/**
* Allow keyboard interaction to scroll through a list of results.
*/
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
hasan-ozaynaci marked this conversation as resolved.
Show resolved Hide resolved
hasan-ozaynaci marked this conversation as resolved.
Show resolved Hide resolved
const container = containerRef.current;

if (event.isDefaultPrevented() || !container) return;

window.requestAnimationFrame(() => {
const element: HTMLInputElement | null = container.querySelector(
'[aria-selected=true]'
);
if (element) {
const top = element.offsetTop - container.scrollTop; // Calculate the space between active element and top of the list
const bottom =
container.scrollTop +
container.clientHeight -
(element.offsetTop + element.clientHeight); // Calculate the space between active element and bottom of the list
if (bottom < 0) container.scrollTop -= bottom;
if (top < 0) container.scrollTop += top;
}
});
};

const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
setInputValue(event.target.value);
}
};

function handleSelect(name: string): void {
const handleSelect = (name: string): void => {
if (!name) {
return;
}
Expand All @@ -85,7 +112,7 @@ export function ComboBox<Option extends TOption>(props: TProps<Option>) {
setTimeout(() => {
inputRef.current?.blur();
}, 1);
}
};

useEffect(() => {
if (!inputRef.current?.value && isLargeScreen && !hasRegionSelected) {
Expand All @@ -99,11 +126,12 @@ export function ComboBox<Option extends TOption>(props: TProps<Option>) {
<ComboboxInput
ref={inputRef}
onChange={handleInputChange}
onKeyDown={onKeyDown}
placeholder={placeholder}
/>
<ComboboxPopover>
{results.length > 0 ? (
<ComboboxList>
<ComboboxList ref={containerRef}>
{results.map((option, index) => (
<StyledComboboxOption
key={`${index}-${option.name}`}
Expand All @@ -120,13 +148,13 @@ export function ComboBox<Option extends TOption>(props: TProps<Option>) {
<ComboBoxStyles />
</Box>
);
}
};

function useSearchedOptions<Option extends TOption>(
const useSearchedOptions = <Option extends TOption>(
term: string,
options: Option[],
sorter?: (a: Option, b: Option) => number
): Option[] {
): Option[] => {
const throttledTerm = useThrottle(term, 100);

return useMemo(
Expand All @@ -138,7 +166,7 @@ function useSearchedOptions<Option extends TOption>(
}),
[throttledTerm, options, sorter]
);
}
};

const StyledComboboxOption = styled(ComboboxOption)<{
isSelectedOption: boolean;
Expand Down