Skip to content

Commit 97f485d

Browse files
fix: exit search on blur [REACT-855] (#3013)
### 🎯 Goal Enable `exitSearchOnBlur` in the demo application, adjust the behavior accordingly (keep the search open if user clicks on filters or clear button as that could be still considered within the bounds of the search input behavior-wise).
1 parent 381d49d commit 97f485d

File tree

6 files changed

+46
-24
lines changed

6 files changed

+46
-24
lines changed

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/components/EventComponent/EventComponent.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React from 'react';
22

33
import type { Event, LocalMessage } from 'stream-chat';
44
import type { TimestampFormatterOptions } from '../../i18n/types';
5-
import { useTranslationContext } from '../../context';
65

76
export type EventComponentProps = TimestampFormatterOptions & {
87
message: LocalMessage & {

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: 21 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,18 @@ 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={({ currentTarget, relatedTarget }) => {
63+
if (
64+
exitSearchOnInputBlur &&
65+
// input is empty
66+
!currentTarget.value &&
67+
// clicking on filter buttons or clear button shouldn't trigger exit search on blur
68+
!filterButtonsContainerRef.current?.contains(relatedTarget) &&
69+
// clicking clear button shouldn't trigger exit search on blur
70+
(!clearButtonRef.current || relatedTarget !== clearButtonRef.current)
71+
) {
72+
searchController.exit();
73+
}
5874
}}
5975
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
6076
if (event.target.value) {
@@ -80,14 +96,14 @@ export const SearchBar = () => {
8096
searchController.clear();
8197
input?.focus();
8298
}}
99+
ref={clearButtonRef}
83100
size='xs'
84101
variant='secondary'
85102
>
86103
<IconCircleX />
87104
</Button>
88105
)}
89106
</div>
90-
{/* TODO: return button once designs are in */}
91107
{isActive && (
92108
<Button
93109
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)