Skip to content

Commit c8c636e

Browse files
Allow exiting search on blur, fix behavior
1 parent f1e9452 commit c8c636e

5 files changed

Lines changed: 44 additions & 23 deletions

File tree

examples/vite/src/ChatLayout/Panels.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ChannelAvatar,
88
ChannelHeader,
99
ChannelList,
10+
ChannelSearchProps,
1011
ChatView,
1112
MessageInput,
1213
MessageList,
@@ -42,6 +43,10 @@ const ChannelThreadPanel = () => {
4243
);
4344
};
4445

46+
const CustomChannelSearch = (props: ChannelSearchProps) => (
47+
<Search {...props} exitSearchOnInputBlur />
48+
);
49+
4550
export const ChannelsPanels = ({
4651
filters,
4752
initialChannelId,
@@ -65,7 +70,7 @@ export const ChannelsPanels = ({
6570
ref={channelsLayoutRef}
6671
>
6772
<ChannelList
68-
ChannelSearch={Search}
73+
ChannelSearch={CustomChannelSearch}
6974
Avatar={ChannelAvatar}
7075
customActiveChannel={initialChannelId}
7176
filters={filters}

src/experimental/Search/Search.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import clsx from 'clsx';
2-
import React from 'react';
2+
import React, { useRef } from 'react';
33
import type { SearchControllerState } from 'stream-chat';
44

55
import { SearchBar as DefaultSearchBar } from './SearchBar/SearchBar';
@@ -17,10 +17,11 @@ const searchControllerStateSelector = (
1717
): SearchControllerStateSelectorReturnValue => ({ isActive: nextValue.isActive });
1818

1919
export type SearchProps = {
20+
/** The type of channel to create on user result select, defaults to `messaging` */
2021
directMessagingChannelType?: string;
2122
/** Sets the input element into disabled state */
2223
disabled?: boolean;
23-
/** Clear search state / results on every click outside the search input, defaults to false */
24+
/** Clear the search state/results on every click outside the search input, defaults to `false` */
2425
exitSearchOnInputBlur?: boolean;
2526
/** Custom placeholder text to be displayed in the search input */
2627
placeholder?: string;
@@ -29,11 +30,13 @@ export type SearchProps = {
2930
export const Search = ({
3031
directMessagingChannelType = 'messaging',
3132
disabled,
32-
exitSearchOnInputBlur,
33+
exitSearchOnInputBlur = false,
3334
placeholder,
3435
}: SearchProps) => {
3536
const { SearchBar = DefaultSearchBar, SearchResults = DefaultSearchResults } =
3637
useComponentContext();
38+
const containerRef = useRef<HTMLDivElement | null>(null);
39+
const filterButtonsContainerRef = useRef<HTMLDivElement | null>(null);
3740

3841
const { searchController } = useChatContext();
3942

@@ -45,9 +48,11 @@ export const Search = ({
4548
return (
4649
<SearchContextProvider
4750
value={{
51+
containerRef,
4852
directMessagingChannelType,
4953
disabled,
5054
exitSearchOnInputBlur,
55+
filterButtonsContainerRef,
5156
placeholder,
5257
searchController,
5358
}}
@@ -57,6 +62,7 @@ export const Search = ({
5762
'str-chat__search--active': isActive,
5863
})}
5964
data-testid='search'
65+
ref={containerRef}
6066
>
6167
<SearchBar />
6268
<SearchResults />

src/experimental/Search/SearchBar/SearchBar.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ const searchControllerStateSelector = (nextValue: SearchControllerState) => ({
1616

1717
export const SearchBar = () => {
1818
const { t } = useTranslationContext();
19-
const { disabled, exitSearchOnInputBlur, placeholder, searchController } =
20-
useSearchContext();
19+
const {
20+
disabled,
21+
exitSearchOnInputBlur,
22+
filterButtonsContainerRef,
23+
placeholder,
24+
searchController,
25+
} = useSearchContext();
2126
const queriesInProgress = useSearchQueriesInProgress(searchController);
27+
const clearButtonRef = React.useRef<HTMLButtonElement | null>(null);
2228

2329
const [input, setInput] = useState<HTMLInputElement | null>(null);
2430
const { isActive, searchQuery } = useStateStore(
@@ -53,8 +59,16 @@ export const SearchBar = () => {
5359
className='str-chat__search-bar__input'
5460
data-testid='search-input'
5561
disabled={disabled}
56-
onBlur={() => {
57-
if (exitSearchOnInputBlur) searchController.exit();
62+
onBlur={({ relatedTarget }) => {
63+
if (
64+
exitSearchOnInputBlur &&
65+
// clicking on filter buttons or clear button shouldn't trigger exit search on blur
66+
!filterButtonsContainerRef.current?.contains(relatedTarget) &&
67+
// clicking clear button shouldn't trigger exit search on blur
68+
(!clearButtonRef.current || relatedTarget !== clearButtonRef.current)
69+
) {
70+
searchController.exit();
71+
}
5872
}}
5973
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
6074
if (event.target.value) {
@@ -80,14 +94,14 @@ export const SearchBar = () => {
8094
searchController.clear();
8195
input?.focus();
8296
}}
97+
ref={clearButtonRef}
8398
size='xs'
8499
variant='secondary'
85100
>
86101
<IconCircleX />
87102
</Button>
88103
)}
89104
</div>
90-
{/* TODO: return button once designs are in */}
91105
{isActive && (
92106
<Button
93107
appearance='ghost'

src/experimental/Search/SearchContext.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import React, { createContext, useContext } from 'react';
22
import type { PropsWithChildren } from 'react';
33
import type { SearchController } from 'stream-chat';
4+
import type { SearchProps } from './Search';
45

56
export type SearchContextValue = {
6-
/** The type of channel to create on user result select, defaults to `messaging` */
7-
directMessagingChannelType: string;
87
/** Instance of the search controller that handles the data management */
98
searchController: SearchController;
10-
/** Sets the input element into disabled state */
11-
disabled?: boolean;
12-
/** Clear search state / results on every click outside the search input, defaults to true */
13-
exitSearchOnInputBlur?: boolean;
14-
/** Custom placeholder text to be displayed in the search input */
15-
placeholder?: string;
16-
};
9+
/** Reference to the container element of the search component */
10+
containerRef: React.RefObject<HTMLDivElement | null>;
11+
/** Reference to the container element of the filter buttons */
12+
filterButtonsContainerRef: React.RefObject<HTMLDivElement | null>;
13+
} & Pick<SearchProps, 'disabled' | 'placeholder'> &
14+
Required<Pick<SearchProps, 'exitSearchOnInputBlur' | 'directMessagingChannelType'>>;
1715

1816
export const SearchContext = createContext<SearchContextValue | undefined>(undefined);
1917

src/experimental/Search/SearchResults/SearchResultsHeader.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const SearchSourceFilterButton = ({ source }: SearchSourceFilterButtonProps) =>
6363
};
6464

6565
export const SearchResultsHeader = () => {
66-
const { searchController } = useSearchContext();
66+
const { filterButtonsContainerRef, searchController } = useSearchContext();
6767

6868
// render nothing if there's only one source (can't change filters)
6969
if (searchController.sources.length < 2) return null;
@@ -73,12 +73,10 @@ export const SearchResultsHeader = () => {
7373
<div
7474
className='str-chat__search-results-header__filter-source-buttons'
7575
data-testid='filter-source-buttons'
76+
ref={filterButtonsContainerRef}
7677
>
7778
{searchController.sources.map((source) => (
78-
<SearchSourceFilterButton
79-
key={`search-source-filter-button-${source.type}`}
80-
source={source}
81-
/>
79+
<SearchSourceFilterButton key={source.type} source={source} />
8280
))}
8381
</div>
8482
</div>

0 commit comments

Comments
 (0)