Skip to content

Commit

Permalink
feat: prepare for new search modal (#11033)
Browse files Browse the repository at this point in the history
* feat: add new search modal

* wip

* feat: add home search input button

* feat: add global search input modal

* feat: add search input modal to search tab

* feat: make header sticky

* chore: use screen instead of modal

* chore: update palette and explict install of collapstible-tab-view

* feat: add search modal screen test

* fix: yarn.lock issues

* feat: use portal instead of modal/screen

* chore: cleanup

* chore: ignore bottom tabs

* chore: add comment about pointer events

* fix: broken tests
  • Loading branch information
MounirDhahri authored Nov 18, 2024
1 parent 45bfb60 commit 8600330
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 29 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"@braze/react-native-sdk": "11.0.0",
"@expo/react-native-action-sheet": "4.0.1",
"@gorhom/bottom-sheet": "5.0.5",
"@gorhom/portal": "1.0.14",
"@invertase/react-native-apple-authentication": "2.1.5",
"@kesha-antonov/react-native-action-cable": "1.1.4",
"@ptomasroos/react-native-multi-slider": "2.2.2",
Expand Down
10 changes: 10 additions & 0 deletions src/app/Components/GlobalSearchInput.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { GlobalSearchInput } from "app/Components/GlobalSearchInput"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"

describe("GlobalSearchInput", () => {
it("renders the search label properly", () => {
renderWithWrappers(<GlobalSearchInput />)

expect(/Search artists, artworks, etc/).toBeTruthy()
})
})
34 changes: 34 additions & 0 deletions src/app/Components/GlobalSearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Flex, RoundSearchInput, Touchable } from "@artsy/palette-mobile"
import { GlobalSearchInputOverlay } from "app/Components/GlobalSearchInputOverlay"
import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search"
import { Fragment, useState } from "react"

export const GlobalSearchInput: React.FC<{}> = () => {
const [isVisible, setIsVisible] = useState(false)

return (
<Fragment>
<Touchable
onPress={() => {
setIsVisible(true)
}}
>
{/* In order to make the search input behave like a button here, we wrapped it with a
Touchable and set pointerEvents to none. This will prevent the input from receiving
touch events and make sure they are being handled by the Touchable.
*/}
<Flex pointerEvents="none">
<RoundSearchInput
placeholder={SEARCH_INPUT_PLACEHOLDER}
accessibilityHint="Search artists, artworks, galleries etc."
accessibilityLabel="Search artists, artworks, galleries etc."
maxLength={55}
numberOfLines={1}
multiline={false}
/>
</Flex>
</Touchable>
<GlobalSearchInputOverlay visible={isVisible} hideModal={() => setIsVisible(false)} />
</Fragment>
)
}
43 changes: 43 additions & 0 deletions src/app/Components/GlobalSearchInputOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Flex, RoundSearchInput, Spacer } from "@artsy/palette-mobile"
import { Portal } from "@gorhom/portal"
import { FadeIn } from "app/Components/FadeIn"
import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search"
import { StyleSheet } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"

export const GlobalSearchInputOverlay: React.FC<{ visible: boolean; hideModal: () => void }> = ({
visible,
hideModal,
}) => {
if (!visible) {
return null
}

return (
<Portal hostName="SearchOverlay">
<FadeIn style={StyleSheet.absoluteFillObject} slide={false}>
<SafeAreaView
style={{ flex: 1, backgroundColor: "white" }}
edges={["top", "left", "right"]}
>
<Flex px={2} pt={2}>
<RoundSearchInput
placeholder={SEARCH_INPUT_PLACEHOLDER}
accessibilityHint="Search artists, artworks, galleries etc."
accessibilityLabel="Search artists, artworks, galleries etc."
maxLength={55}
numberOfLines={1}
autoFocus
multiline={false}
onLeftIconPress={() => {
hideModal()
}}
/>
</Flex>
<Spacer y={2} />
<Flex flex={1} backgroundColor="black10" />
</SafeAreaView>
</FadeIn>
</Portal>
)
}
2 changes: 2 additions & 0 deletions src/app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Theme, Spinner, ScreenDimensionsProvider, Screen } from "@artsy/palette-mobile"
import { ActionSheetProvider } from "@expo/react-native-action-sheet"
import { PortalProvider } from "@gorhom/portal"
import { ArtworkListsProvider } from "app/Components/ArtworkLists/ArtworkListsContext"
import { ShareSheetProvider } from "app/Components/ShareSheet/ShareSheetContext"
import { getRelayEnvironment } from "app/system/relay/defaultEnvironment"
Expand Down Expand Up @@ -58,6 +59,7 @@ export const TestProviders: React.FC<{ skipRelay?: boolean }> = ({
TrackingProvider,
GlobalStoreProvider,
SafeAreaProvider,
PortalProvider,
ProvideScreenDimensions,
// FIXME: Only use one from palette-mobile
// @ts-ignore
Expand Down
34 changes: 32 additions & 2 deletions src/app/Scenes/HomeView/Components/HomeHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
import { ArtsyLogoBlackIcon, Flex, Box } from "@artsy/palette-mobile"
import { ArtsyLogoBlackIcon, Box, Flex } from "@artsy/palette-mobile"
import { GlobalSearchInput } from "app/Components/GlobalSearchInput"
import { PaymentFailureBanner } from "app/Scenes/HomeView/Components/PaymentFailureBanner"
import { GlobalStore } from "app/store/GlobalStore"
import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag"
import { Suspense } from "react"
import { ActivityIndicator } from "./ActivityIndicator"

export const HomeHeader: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
const showPaymentFailureBanner = useFeatureFlag("AREnablePaymentFailureBanner")
const hasUnseenNotifications = GlobalStore.useAppState(
(state) => state.bottomTabs.hasUnseenNotifications
)

if (enableNewSearchModal) {
return (
<Flex backgroundColor="white100">
{!!showPaymentFailureBanner && (
<Suspense fallback={null}>
<PaymentFailureBanner />
</Suspense>
)}
<Flex py={2}>
<Flex
flexDirection="row"
px={2}
gap={2}
justifyContent="space-around"
alignItems="center"
>
<Flex flex={1}>
<GlobalSearchInput />
</Flex>
<Flex alignItems="flex-end">
<ActivityIndicator hasUnseenNotifications={hasUnseenNotifications} />
</Flex>
</Flex>
</Flex>
</Flex>
)
}

return (
<>
{!!showPaymentFailureBanner && (
<Suspense fallback={null}>
<PaymentFailureBanner />
</Suspense>
)}
<Box py={2}>
<Box py={2} backgroundColor="white100">
<Flex flexDirection="row" px={2} justifyContent="space-between" alignItems="center">
<Box flex={1} />
<Box>
Expand Down
15 changes: 15 additions & 0 deletions src/app/Scenes/HomeView/HomeView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContextModule, OwnerType } from "@artsy/cohesion"
import { Flex, Screen, Spinner } from "@artsy/palette-mobile"
import { PortalHost } from "@gorhom/portal"
import { useFocusEffect } from "@react-navigation/native"
import { HomeViewFetchMeQuery } from "__generated__/HomeViewFetchMeQuery.graphql"
import { HomeViewQuery } from "__generated__/HomeViewQuery.graphql"
Expand All @@ -19,6 +20,7 @@ import { getRelayEnvironment } from "app/system/relay/defaultEnvironment"
import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper"
import { useExperimentVariant } from "app/utils/experiments/hooks"
import { extractNodes } from "app/utils/extractNodes"
import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag"
import { ProvidePlaceholderContext } from "app/utils/placeholders"
import { usePrefetch } from "app/utils/queryPrefetching"
import { requestPushNotificationsPermission } from "app/utils/requestPushNotificationsPermission"
Expand All @@ -34,6 +36,7 @@ export const homeViewScreenQueryVariables = () => ({
})

export const HomeView: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
const flashlistRef = useBottomTabsScrollToTop("home")
const [isRefreshing, setIsRefreshing] = useState(false)

Expand Down Expand Up @@ -139,10 +142,20 @@ export const HomeView: React.FC = () => {
})
}

const stickyHeaderProps = enableNewSearchModal
? {
stickyHeaderHiddenOnScroll: true,
stickyHeaderIndices: [0],
}
: {}

return (
<Screen safeArea={true}>
<Screen.Body fullwidth>
<FlatList
automaticallyAdjustKeyboardInsets
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
ref={flashlistRef as RefObject<FlatList>}
data={sections}
Expand All @@ -161,6 +174,7 @@ export const HomeView: React.FC = () => {
}
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />}
onEndReachedThreshold={2}
{...stickyHeaderProps}
/>
{!!data?.me && <EmailConfirmationBannerFragmentContainer me={data.me} />}
</Screen.Body>
Expand Down Expand Up @@ -189,6 +203,7 @@ export const HomeViewScreen: React.FC = () => {
<Suspense fallback={<HomeViewScreenPlaceholder />}>
<HomeView />
</Suspense>
<PortalHost name="SearchOverlay" />
</RetryErrorBoundary>
</HomeViewStoreProvider>
)
Expand Down
10 changes: 5 additions & 5 deletions src/app/Scenes/Search/Search.tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("Search", () => {
it("should render a text input with placeholder and no pills", async () => {
const { env } = renderWithRelay()

const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc")
const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc")

expect(searchInput).toBeTruthy()

Expand All @@ -43,7 +43,7 @@ describe("Search", () => {
it("Top pill should be selected by default", async () => {
const { env } = renderWithRelay()

const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc")
const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc")

fireEvent.changeText(searchInput, "text")

Expand All @@ -56,7 +56,7 @@ describe("Search", () => {
it("when clear button is pressed", async () => {
const { env } = renderWithRelay()

const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc")
const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc")

fireEvent(searchInput, "changeText", "prev value")

Expand All @@ -77,7 +77,7 @@ describe("Search", () => {
it("when cancel button is pressed", async () => {
const { env } = renderWithRelay()

const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc")
const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc")

fireEvent(searchInput, "changeText", "prev value")
// needed to resolve the relay operation triggered for the text change
Expand All @@ -98,7 +98,7 @@ describe("Search", () => {
it("should render all the default pills", async () => {
const { env } = renderWithRelay()

const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc")
const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc")

fireEvent(searchInput, "changeText", "Ba")

Expand Down
52 changes: 34 additions & 18 deletions src/app/Scenes/Search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion"
import { Spacer, Flex, Box, Screen } from "@artsy/palette-mobile"
import { PortalHost } from "@gorhom/portal"
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { SearchQuery, SearchQuery$variables } from "__generated__/SearchQuery.graphql"
import { GlobalSearchInput } from "app/Components/GlobalSearchInput"
import { SearchInput } from "app/Components/SearchInput"
import { SearchPills } from "app/Scenes/Search/SearchPills"
import { useRefetchWhenQueryChanged } from "app/Scenes/Search/useRefetchWhenQueryChanged"
import { useSearchQuery } from "app/Scenes/Search/useSearchQuery"
import { ArtsyKeyboardAvoidingView } from "app/utils/ArtsyKeyboardAvoidingView"
import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper"
import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag"
import { Schema } from "app/utils/track"
import { throttle } from "lodash"
import { Suspense, useEffect, useMemo, useRef, useState } from "react"
Expand All @@ -28,8 +31,7 @@ import { SEARCH_PILLS, SEARCH_THROTTLE_INTERVAL, TOP_PILL } from "./constants"
import { getContextModuleByPillName } from "./helpers"
import { PillType } from "./types"

const SEARCH_INPUT_PLACEHOLDER = [
"Search artists, artworks, galleries, etc",
export const SEARCH_INPUT_PLACEHOLDER = [
"Search artists, artworks, etc",
"Search artworks, etc",
"Search",
Expand All @@ -41,6 +43,7 @@ export const searchQueryDefaultVariables: SearchQuery$variables = {
}

export const Search: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
const searchPillsRef = useRef<ScrollView>(null)
const [searchQuery, setSearchQuery] = useState<string>("")
const [selectedPill, setSelectedPill] = useState<PillType>(TOP_PILL)
Expand Down Expand Up @@ -130,11 +133,15 @@ export const Search: React.FC = () => {
<SearchContext.Provider value={searchProviderValues}>
<ArtsyKeyboardAvoidingView>
<Flex p={2} pb={0}>
<SearchInput
ref={searchProviderValues?.inputRef}
placeholder={SEARCH_INPUT_PLACEHOLDER}
onChangeText={onSearchTextChanged}
/>
{enableNewSearchModal ? (
<GlobalSearchInput />
) : (
<SearchInput
ref={searchProviderValues?.inputRef}
placeholder={SEARCH_INPUT_PLACEHOLDER}
onChangeText={onSearchTextChanged}
/>
)}
</Flex>
<Flex flex={1} collapsable={false}>
{shouldStartSearching(searchQuery) && !!queryData.viewer ? (
Expand All @@ -158,11 +165,15 @@ export const Search: React.FC = () => {
</>
) : (
<Scrollable ref={scrollableRef}>
<HorizontalPadding>
<RecentSearches />
</HorizontalPadding>
{!enableNewSearchModal && (
<>
<HorizontalPadding>
<RecentSearches />
</HorizontalPadding>
<Spacer y={4} />
</>
)}

<Spacer y={4} />
<TrendingArtists data={queryData} mb={4} />
<CuratedCollections collections={queryData} mb={4} />

Expand All @@ -189,13 +200,18 @@ export const SearchScreenQuery = graphql`

type SearchScreenProps = StackScreenProps<any>

export const SearchScreen: React.FC<SearchScreenProps> = () => (
<Screen>
<Suspense fallback={<SearchPlaceholder />}>
<Search />
</Suspense>
</Screen>
)
export const SearchScreen: React.FC<SearchScreenProps> = () => {
return (
<>
<Screen>
<Suspense fallback={<SearchPlaceholder />}>
<Search />
</Suspense>
</Screen>
<PortalHost name="SearchOverlay" />
</>
)
}

const Scrollable = styled(ScrollView).attrs(() => ({
keyboardDismissMode: "on-drag",
Expand Down
Loading

0 comments on commit 8600330

Please sign in to comment.