Skip to content

Commit

Permalink
fix click selection behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
alixlahuec committed Aug 2, 2024
1 parent 35f496d commit 6ba2eb1
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 26 deletions.
8 changes: 6 additions & 2 deletions src/components/Dashboard/Explorer/QueryBar/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const WithInteractions: StoryObj<Props> = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const searchbar = canvas.getByTestId("explorer-searchbar");
const searchbar = canvas.getByTestId("explorer-searchbar") as HTMLInputElement;

await userEvent.click(searchbar);
await waitFor(() =>
Expand All @@ -105,6 +105,10 @@ export const WithInteractions: StoryObj<Props> = {
expect(suggestions[0]).toHaveAttribute("aria-selected", "true");

await userEvent.click(suggestions[0]);
await expect(searchbar).toHaveValue(filters[0].value + ":" + filters[0].presets[0].value);
const expectedNewQuery = filters[0].value + ":" + filters[0].presets[0].value;
await expect(searchbar).toHaveValue(expectedNewQuery);

await expect(searchbar).toHaveFocus();
await expect(searchbar.selectionStart).toEqual(expectedNewQuery.length);
}
};
27 changes: 20 additions & 7 deletions src/components/Dashboard/Explorer/QueryBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Classes, Icon, InputGroup, MenuItem } from "@blueprintjs/core";
import { Classes, Icon, InputGroup, InputGroupProps2, MenuItem } from "@blueprintjs/core";
import { QueryList, QueryListProps } from "@blueprintjs/select";

import { QueryFilter, SearchSuggestion, useSearchFilters } from "@hooks";
Expand Down Expand Up @@ -46,28 +46,41 @@ type Props<T extends Record<string, any> = Record<string, any>> = {

function ExplorerQueryList<T extends Record<string, any>>({ filters, onQueryChange, query }: Props<T>) {
const searchbar = useRef<HTMLInputElement>(null);
const [cursorPosition, setCursorPosition] = useState(() => searchbar.current?.selectionStart || 0);
const [cursorPosition, updateCursorPosition] = useState(() => searchbar.current?.selectionStart || 0);
const [showSuggestions, setShowSuggestions] = useState(false);

const refreshCursorPosition = useCallback(() => {
const posWithinSearchbar = searchbar.current?.selectionStart;
if (posWithinSearchbar !== undefined && posWithinSearchbar !== null) setCursorPosition(posWithinSearchbar);
}, [searchbar, setCursorPosition]);
if (posWithinSearchbar !== undefined && posWithinSearchbar !== null) updateCursorPosition(posWithinSearchbar);
}, [searchbar, updateCursorPosition]);

const setCursorPosition = useCallback((pos: number) => {
searchbar.current?.focus();
searchbar.current?.setSelectionRange(pos, null, "none");
refreshCursorPosition();
}, [searchbar, refreshCursorPosition]);

const handleQueryChange = useCallback<Required<QueryListProps<SearchSuggestion<T>>>["onQueryChange"]>((query) => {
refreshCursorPosition();
onQueryChange(query);
}, [onQueryChange, refreshCursorPosition]);

const { applySuggestion, suggestions } = useSearchFilters<T>({ cursorPosition, filters, handleQueryChange, query });
const { applySuggestion, suggestions } = useSearchFilters<T>({ cursorPosition, filters, handleQueryChange, query, setCursorPosition });

const handleItemSelect = useCallback<QueryListProps<SearchSuggestion<T>>["onItemSelect"]>((item, _e) => {
applySuggestion(item);
}, [applySuggestion, handleQueryChange]);
}, [applySuggestion]);

const listRenderer = useCallback<QueryListProps<SearchSuggestion<T>>["renderer"]>((listProps) => {
const { handleKeyUp, handleKeyDown, handleQueryChange: onChange, itemList, query } = listProps;

const handleBlur: InputGroupProps2["onBlur"] = (e) => {
if (e.relatedTarget?.closest(`#zr-explorer-suggestions`)) {
return
}
setShowSuggestions(false)
}

return <>
<InputGroup
autoComplete="off"
Expand All @@ -76,7 +89,7 @@ function ExplorerQueryList<T extends Record<string, any>>({ filters, onQueryChan
id="zr-explorer-searchbar"
inputRef={searchbar}
leftElement={searchbarLeftElement}
onBlur={() => setShowSuggestions(false)}
onBlur={handleBlur}
onChange={onChange}
onFocus={() => setShowSuggestions(true)}
onKeyDown={handleKeyDown}
Expand Down
33 changes: 19 additions & 14 deletions src/hooks/useSearch/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ const filters: QueryFilter<Item>[] = [
];

const handleQueryChange = vi.fn();
const setCursorPosition = vi.fn();


describe("useSearchFilters", () => {
it("returns all filters when the query is empty", async () => {
const { result, waitFor } = renderHook(() => useSearchFilters({ query: "", cursorPosition: 0, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query: "", cursorPosition: 0, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand All @@ -63,7 +64,7 @@ describe("useSearchFilters", () => {
test.each(firstTermCases)(
"returns no suggestions - cursor at %s",
async (cursorPosition) => {
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand All @@ -82,7 +83,7 @@ describe("useSearchFilters", () => {
test.each(secondTermCases)(
"returns no suggestions - cursor at %s",
async (cursorPosition) => {
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand All @@ -102,7 +103,7 @@ describe("useSearchFilters", () => {
describe("with fully qualified query and a trailing space", () => {
it("returns all filters", async () => {
const query = "roam:true ";
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition: 10, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition: 10, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand Down Expand Up @@ -130,7 +131,7 @@ describe("useSearchFilters", () => {
test.each(cases)(
"returns filters with matching name - %# - $expectedSuggestions.length results",
async ({ cursorPosition, expectedSuggestions }) => {
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand All @@ -148,7 +149,7 @@ describe("useSearchFilters", () => {

it("returns presets when the user has selected an operator", async () => {
const query = "roam:";
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition: 5, filters, handleQueryChange }));
const { result, waitFor } = renderHook(() => useSearchFilters({ query, cursorPosition: 5, filters, handleQueryChange, setCursorPosition }));

await waitFor(() => expect(result.current.terms).toBeDefined());

Expand Down Expand Up @@ -178,14 +179,17 @@ describe("useSearchFilters", () => {
test.each(cases)(
"%# - $query, cursor at $cursorPosition -> $suggestion.value",
async ({ query, cursorPosition, suggestion }) => {
const { result, waitFor } = renderHook(() => useSearchFilters({ cursorPosition, query, handleQueryChange, filters }));
const { result, waitFor } = renderHook(() => useSearchFilters({ cursorPosition, query, handleQueryChange, filters, setCursorPosition }));

await waitFor(() => expect(result.current.suggestions).toBeDefined());

act(() => result.current.applySuggestion(suggestion));

expect(handleQueryChange.mock.calls).toHaveLength(1);
expect(handleQueryChange.mock.calls[0]).toEqual([suggestion.value + ":"]);
expect(handleQueryChange).toBeCalledTimes(1);
expect(handleQueryChange).toBeCalledWith(suggestion.value + ":");

expect(setCursorPosition).toBeCalledTimes(1);
expect(setCursorPosition).toBeCalledWith(suggestion.value.length + 1);
}
)
});
Expand All @@ -202,16 +206,17 @@ describe("useSearchFilters", () => {
test.each(cases)(
"%# - $query $suggestion.value",
async ({ query, suggestion }) => {
const { result, waitFor } = renderHook(() => useSearchFilters({ cursorPosition: query.length, query, handleQueryChange, filters }));
const { result, waitFor } = renderHook(() => useSearchFilters({ cursorPosition: query.length, query, handleQueryChange, filters, setCursorPosition }));

await waitFor(() => expect(result.current.suggestions).toBeDefined());

console.log({ result: result.current });
act(() => result.current.applySuggestion(suggestion));
console.log({ result: result.current });

expect(handleQueryChange.mock.calls).toHaveLength(1);
expect(handleQueryChange.mock.calls[0]).toEqual([query + suggestion.value]);
expect(handleQueryChange).toBeCalledTimes(1);
expect(handleQueryChange).toBeCalledWith(query + suggestion.value);

expect(setCursorPosition).toBeCalledTimes(1);
expect(setCursorPosition).toBeCalledWith(query.length + suggestion.value.length)
}
)
});
Expand Down
10 changes: 7 additions & 3 deletions src/hooks/useSearch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ type UseSearchProps<T extends Record<string, any> = Record<string, any>> = {
cursorPosition: number,
filters: QueryFilter<T>[],
handleQueryChange: (query: string) => void,
query: string
query: string,
setCursorPosition: (pos: number) => void
};

const useSearchFilters = <T extends Record<string, any> = Record<string ,any>>(
{ cursorPosition, filters, handleQueryChange, query }: UseSearchProps<T>
{ cursorPosition, filters, handleQueryChange, query, setCursorPosition }: UseSearchProps<T>
) => {
const terms = useMemo(() => parseQueryTerms(query), [query]);

Expand All @@ -38,8 +39,11 @@ const useSearchFilters = <T extends Record<string, any> = Record<string ,any>>(
const termsList = terms;
termsList[termIndex] = updatedFilter;

const newCursorPosition = termsList.map(term => term.length).filter((_term, index) => index <= termIndex).reduce((iter, termLength) => iter + termLength, 0);

handleQueryChange(termsList.join(""));
}, [currentPositionDetails, handleQueryChange]);
setCursorPosition(newCursorPosition);
}, [currentPositionDetails, handleQueryChange, setCursorPosition]);

return {
/**
Expand Down

0 comments on commit 6ba2eb1

Please sign in to comment.