Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion examples/vite/src/ChatLayout/Panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ChannelAvatar,
ChannelHeader,
ChannelList,
ChannelSearchProps,
ChatView,
MessageInput,
MessageList,
Expand Down Expand Up @@ -42,6 +43,10 @@ const ChannelThreadPanel = () => {
);
};

const CustomChannelSearch = (props: ChannelSearchProps) => (
<Search {...props} exitSearchOnInputBlur />
);

export const ChannelsPanels = ({
filters,
initialChannelId,
Expand All @@ -65,7 +70,7 @@ export const ChannelsPanels = ({
ref={channelsLayoutRef}
>
<ChannelList
ChannelSearch={Search}
ChannelSearch={CustomChannelSearch}
Avatar={ChannelAvatar}
customActiveChannel={initialChannelId}
filters={filters}
Expand Down
1 change: 0 additions & 1 deletion src/components/EventComponent/EventComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';

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

export type EventComponentProps = TimestampFormatterOptions & {
message: LocalMessage & {
Expand Down
12 changes: 9 additions & 3 deletions src/experimental/Search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { useRef } from 'react';
import type { SearchControllerState } from 'stream-chat';

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

export type SearchProps = {
/** The type of channel to create on user result select, defaults to `messaging` */
directMessagingChannelType?: string;
/** Sets the input element into disabled state */
disabled?: boolean;
/** Clear search state / results on every click outside the search input, defaults to false */
/** Clear the search state/results on every click outside the search input, defaults to `false` */
exitSearchOnInputBlur?: boolean;
/** Custom placeholder text to be displayed in the search input */
placeholder?: string;
Expand All @@ -29,11 +30,13 @@ export type SearchProps = {
export const Search = ({
directMessagingChannelType = 'messaging',
disabled,
exitSearchOnInputBlur,
exitSearchOnInputBlur = false,
placeholder,
}: SearchProps) => {
const { SearchBar = DefaultSearchBar, SearchResults = DefaultSearchResults } =
useComponentContext();
const containerRef = useRef<HTMLDivElement | null>(null);
const filterButtonsContainerRef = useRef<HTMLDivElement | null>(null);

const { searchController } = useChatContext();

Expand All @@ -45,9 +48,11 @@ export const Search = ({
return (
<SearchContextProvider
value={{
containerRef,
directMessagingChannelType,
disabled,
exitSearchOnInputBlur,
filterButtonsContainerRef,
placeholder,
searchController,
}}
Expand All @@ -57,6 +62,7 @@ export const Search = ({
'str-chat__search--active': isActive,
})}
data-testid='search'
ref={containerRef}
>
<SearchBar />
<SearchResults />
Expand Down
26 changes: 21 additions & 5 deletions src/experimental/Search/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ const searchControllerStateSelector = (nextValue: SearchControllerState) => ({

export const SearchBar = () => {
const { t } = useTranslationContext();
const { disabled, exitSearchOnInputBlur, placeholder, searchController } =
useSearchContext();
const {
disabled,
exitSearchOnInputBlur,
filterButtonsContainerRef,
placeholder,
searchController,
} = useSearchContext();
const queriesInProgress = useSearchQueriesInProgress(searchController);
const clearButtonRef = React.useRef<HTMLButtonElement | null>(null);

const [input, setInput] = useState<HTMLInputElement | null>(null);
const { isActive, searchQuery } = useStateStore(
Expand Down Expand Up @@ -53,8 +59,18 @@ export const SearchBar = () => {
className='str-chat__search-bar__input'
data-testid='search-input'
disabled={disabled}
onBlur={() => {
if (exitSearchOnInputBlur) searchController.exit();
onBlur={({ currentTarget, relatedTarget }) => {
if (
exitSearchOnInputBlur &&
// input is empty
!currentTarget.value &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on the verge regarding this value check addition but the ticket calls for it so I've added it in a separate commit, let me know how you guys feel about it.

// clicking on filter buttons or clear button shouldn't trigger exit search on blur
!filterButtonsContainerRef.current?.contains(relatedTarget) &&
// clicking clear button shouldn't trigger exit search on blur
(!clearButtonRef.current || relatedTarget !== clearButtonRef.current)
) {
searchController.exit();
}
}}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
Expand All @@ -80,14 +96,14 @@ export const SearchBar = () => {
searchController.clear();
input?.focus();
}}
ref={clearButtonRef}
size='xs'
variant='secondary'
>
<IconCircleX />
</Button>
)}
</div>
{/* TODO: return button once designs are in */}
{isActive && (
<Button
appearance='ghost'
Expand Down
16 changes: 7 additions & 9 deletions src/experimental/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import React, { createContext, useContext } from 'react';
import type { PropsWithChildren } from 'react';
import type { SearchController } from 'stream-chat';
import type { SearchProps } from './Search';

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const SearchSourceFilterButton = ({ source }: SearchSourceFilterButtonProps) =>
};

export const SearchResultsHeader = () => {
const { searchController } = useSearchContext();
const { filterButtonsContainerRef, searchController } = useSearchContext();

// render nothing if there's only one source (can't change filters)
if (searchController.sources.length < 2) return null;
Expand All @@ -73,12 +73,10 @@ export const SearchResultsHeader = () => {
<div
className='str-chat__search-results-header__filter-source-buttons'
data-testid='filter-source-buttons'
ref={filterButtonsContainerRef}
>
{searchController.sources.map((source) => (
<SearchSourceFilterButton
key={`search-source-filter-button-${source.type}`}
source={source}
/>
<SearchSourceFilterButton key={source.type} source={source} />
))}
</div>
</div>
Expand Down
Loading