diff --git a/docusaurus/docs/React/custom-code-examples/channel-search.mdx b/docusaurus/docs/React/custom-code-examples/channel-search.mdx index 7490bdf8b7..6d47b9b865 100644 --- a/docusaurus/docs/React/custom-code-examples/channel-search.mdx +++ b/docusaurus/docs/React/custom-code-examples/channel-search.mdx @@ -339,3 +339,92 @@ const customSearchFunction = async (props: ChannelSearchFunctionParams, event: { additionalChannelSearchProps={{searchFunction: customSearchFunction}} /> ``` + +### Adding menu + +As of the version 10.0.0, users can add app menu into the `SearchBar`. In case you would like to display menu button next to the search input, you can do that by adding [`AppMenu` component](../utility-components/channel-search.mdx/#appmenu) to the `ChannelSearch` props. The display of `AppMenu` is then toggled by clicking on the menu button. `AppMenu` can be rendered as a drop-down or even a modal. In our example we will render a drop-down menu. + +:::caution +The SDK does not provide any default `AppMenu` component and so you will have to write your CSS for it to be styled correctly. +::: + +```tsx +import React, { useCallback } from 'react'; +import type { AppMenuProps } from 'stream-chat-react'; + +import './AppMenu.scss'; + +export const AppMenu = ({close}: AppMenuProps) => { + + const handleSelect = useCallback(() => { + // custom logic... + close?.(); + }, [close]); + + return ( +
+ +
+ ); +} +``` + +```scss +.str-chat__channel-search-bar-button.str-chat__channel-search-bar-button--menu { + position: relative; +} + +.app-menu { + &__container { + position: absolute; + top: 50px; + left: 10px; + background-color: white; + border-radius: 5px; + box-shadow: 0 0 8px var(--str-chat__box-shadow-color); + } + + &__item-list { + list-style: none; + margin: 0; + padding: 0; + } + + &__item { + list-style: none; + margin: 0; + padding: .5rem 1rem; + + &:hover { + background-color: lightgrey; + cursor: pointer; + } + } +} +``` + +```jsx +import { AppMenu } from './components/AppMenu'; + +const App = () => ( + + + + + + + + + + + +); +``` diff --git a/docusaurus/docs/React/utility-components/channel-search.mdx b/docusaurus/docs/React/utility-components/channel-search.mdx index 2e635f4be8..19ecef47d3 100644 --- a/docusaurus/docs/React/utility-components/channel-search.mdx +++ b/docusaurus/docs/React/utility-components/channel-search.mdx @@ -158,11 +158,11 @@ The `ChannelSearch` offers possibility to keep the search results open meanwhile ### AppMenu -Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is rendered with `themeVersion` `'2'` only. No default component provided by the SDK. The library does not provide any CSS for `AppMenu`. +Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is only available with the use of the [theming v2](../theming/introduction.mdx). No default component is provided by the SDK. The library does not provide any CSS for `AppMenu`. Consult the customization tutorial on how to [add AppMenu to your application](../custom-code-examples/channel-search.mdx/#adding-menu). The component is passed a prop `close`, which is a function that can be called to hide the app menu (e.g. on menu item selection). -| Type | Default | -| ------------------- | ------------ | -| `React.ComponentType` | `undefined` | +| Type | Default | +|-----------------------|-------------| +| `React.ComponentType` | `undefined` | ### channelType diff --git a/package.json b/package.json index fa407f47c2..65c15616e4 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@braintree/sanitize-url": "6.0.0", "@popperjs/core": "^2.11.5", - "@stream-io/stream-chat-css": "^3.1.0", + "@stream-io/stream-chat-css": "3.1.1", "clsx": "^1.1.1", "dayjs": "^1.10.4", "emoji-mart": "3.0.1", diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts index 14665451b5..4f9dc5f3d0 100644 --- a/src/components/ChannelList/hooks/usePaginatedChannels.ts +++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts @@ -26,6 +26,7 @@ export const usePaginatedChannels = < const [channels, setChannels] = useState>>([]); const [hasNextPage, setHasNextPage] = useState(true); + // memoize props const filterString = useMemo(() => JSON.stringify(filters), [filters]); const sortString = useMemo(() => JSON.stringify(sort), [sort]); diff --git a/src/components/ChannelSearch/SearchBar.tsx b/src/components/ChannelSearch/SearchBar.tsx index 83d679767f..40aa584900 100644 --- a/src/components/ChannelSearch/SearchBar.tsx +++ b/src/components/ChannelSearch/SearchBar.tsx @@ -16,6 +16,10 @@ import { } from './icons'; import { SearchInput as DefaultSearchInput, SearchInputProps } from './SearchInput'; +export type AppMenuProps = { + close?: () => void; +}; + type SearchBarButtonProps = { className?: string; onClick?: MouseEventHandler; @@ -48,7 +52,7 @@ export type SearchBarController = { export type AdditionalSearchBarProps = { /** Application menu to be displayed when clicked on MenuIcon */ - AppMenu?: React.ComponentType; + AppMenu?: React.ComponentType; /** Custom icon component used to clear the input value on click. Displayed within the search input wrapper. */ ClearInputIcon?: React.ComponentType; /** Custom icon component used to terminate the search UI session on click. */ @@ -133,6 +137,8 @@ export const SearchBar = (props: SearchBarProps) => { inputProps.inputRef.current?.focus(); }, []); + const closeAppMenu = useCallback(() => setMenuIsOpen(false), []); + return (
{inputIsFocused ? ( @@ -172,7 +178,7 @@ export const SearchBar = (props: SearchBarProps) => {
{menuIsOpen && AppMenu && (
- +
)} diff --git a/src/components/ChannelSearch/__tests__/SearchBar.test.js b/src/components/ChannelSearch/__tests__/SearchBar.test.js index 977b4deea5..9632443b14 100644 --- a/src/components/ChannelSearch/__tests__/SearchBar.test.js +++ b/src/components/ChannelSearch/__tests__/SearchBar.test.js @@ -22,7 +22,12 @@ jest.spyOn(window, 'getComputedStyle').mockReturnValue({ let client; const inputText = new Date().getTime().toString(); -const AppMenu = () =>
AppMenu
; +const AppMenu = ({ close }) => ( +
+ AppMenu +
+
+); const ClearInputIcon = () =>
CustomClearInputIcon
; const MenuIcon = () =>
CustomMenuIcon
; const SearchInputIcon = () =>
CustomSearchInputIcon
; @@ -301,6 +306,30 @@ describe('SearchBar', () => { }); }); + it('should close the app menu on menu item click', async () => { + await act(() => { + renderComponent({ + client, + props: { AppMenu }, + searchParams: { disabled: false }, + }); + }); + const menuIcon = screen.queryByTestId('menu-icon'); + await act(() => { + fireEvent.click(menuIcon); + }); + + const menuItem = screen.queryByTestId('menu-item'); + + await act(() => { + fireEvent.click(menuItem); + }); + + await waitFor(() => { + expect(screen.queryByText('AppMenu')).not.toBeInTheDocument(); + }); + }); + it.each([ [ 'on click outside', diff --git a/src/components/ChannelSearch/index.ts b/src/components/ChannelSearch/index.ts index e8561aae22..ee626c4f79 100644 --- a/src/components/ChannelSearch/index.ts +++ b/src/components/ChannelSearch/index.ts @@ -1,4 +1,5 @@ export * from './ChannelSearch'; +export * from './SearchBar'; export * from './SearchInput'; export * from './SearchResults'; export * from './utils'; diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index f1bfcaf033..1abcc1886e 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -196,7 +196,7 @@ const VirtualizedMessageListWithContext = < const groupStylesFn = groupStyles || getGroupStyles; const messageGroupStyles = useMemo( () => - processedMessages.reduce((acc, message, i) => { + processedMessages.reduce>((acc, message, i) => { const style = groupStylesFn( message, processedMessages[i - 1], @@ -205,7 +205,8 @@ const VirtualizedMessageListWithContext = < ); if (style) acc[message.id] = style; return acc; - }, {} as Record), + }, {}), + // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage [processedMessages.length, shouldGroupByUser], ); @@ -234,6 +235,7 @@ const VirtualizedMessageListWithContext = < virtuoso, processedMessages, setNewMessagesNotification, + // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage processedMessages.length, hasMoreNewer, jumpToLatestMessage, @@ -375,8 +377,9 @@ const VirtualizedMessageListWithContext = < return Item; }, [ customClasses?.virtualMessage, - Object.keys(messageGroupStyles), + messageGroupStyles, numItemsPrepended, + // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage processedMessages.length, ]); diff --git a/src/components/MessageList/hooks/useEnrichedMessages.ts b/src/components/MessageList/hooks/useEnrichedMessages.ts index 95d24b9731..7db78b4486 100644 --- a/src/components/MessageList/hooks/useEnrichedMessages.ts +++ b/src/components/MessageList/hooks/useEnrichedMessages.ts @@ -65,7 +65,7 @@ export const useEnrichedMessages = < const groupStylesFn = groupStyles || getGroupStyles; const messageGroupStyles = useMemo( () => - messagesWithDates.reduce((acc, message, i) => { + messagesWithDates.reduce>((acc, message, i) => { const style = groupStylesFn( message, messagesWithDates[i - 1], @@ -74,7 +74,7 @@ export const useEnrichedMessages = < ); if (style) acc[message.id] = style; return acc; - }, {} as Record), + }, {}), [messagesWithDates, noGroupByUser], ); diff --git a/src/components/Reactions/hooks/useProcessReactions.tsx b/src/components/Reactions/hooks/useProcessReactions.tsx index bc15e2e2d3..d253ebba14 100644 --- a/src/components/Reactions/hooks/useProcessReactions.tsx +++ b/src/components/Reactions/hooks/useProcessReactions.tsx @@ -55,21 +55,21 @@ export const useProcessReactions = < const latestReactionTypes = useMemo( () => - latestReactions.reduce((reactionTypes, { type }) => { + latestReactions.reduce((reactionTypes, { type }) => { if (reactionTypes.indexOf(type) === -1) { reactionTypes.push(type); } return reactionTypes; - }, [] as string[]), + }, []), [latestReactions], ); const supportedReactionMap = useMemo( () => - reactionOptions.reduce((acc, { id }) => { + reactionOptions.reduce>((acc, { id }) => { acc[id] = true; return acc; - }, {} as Record), + }, {}), [reactionOptions], ); diff --git a/yarn.lock b/yarn.lock index 655988bf52..5d56ee00f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,10 +2078,10 @@ crypto-browserify "^3.11.0" process-es6 "^0.11.2" -"@stream-io/stream-chat-css@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-3.1.0.tgz#f04c71f7eff7c98801656a4c791b19f25f1114f4" - integrity sha512-rcyKzSsA4NGbOjzdDGayFHEJNX8/a8dtwZkVVIhqaIc7lO1Zpoph3wVYByKmtQxHl1SUIw/LYJLf0SHfsTZlYg== +"@stream-io/stream-chat-css@3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-3.1.1.tgz#c19271b12cfb5a1f62bd5e2547d7be1ab7fc49b1" + integrity sha512-/K8QJGeIqluDpTvCmimenZRnET/LMMmqo5a/8Wam+gupHPTOBWB5+noB3x3FlKzg0w1CuKEufdRK1BZxhQK7wA== "@stream-io/transliterate@^1.5.5": version "1.5.5"