Skip to content

Commit 5d77b5a

Browse files
@18981837 search suggestions enhancements v2 (#2901)
* @W-18981837 UX search changes * @W-18981837 search suggestion * @W-18981837 view all button * W-18981837 search suggestions badges impl * W-18981837 new tests and lint * W-18981837 changelog * @W-18981837 translations * W-18981837 bundlesize ci error * @W-18981837 only support string and boolean * W-18981837 remove badges * W-18981837 dynamic image * @W-18981744 adjust widths * @W-18981837 lint fix * @W-18981837 expand required for images
1 parent e5fed7c commit 5d77b5a

File tree

13 files changed

+343
-113
lines changed

13 files changed

+343
-113
lines changed

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
- Use `<picture>` element for responsive images [#2724](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2724)
2626
- Add Data Cloud partyIdentification events and improve error handling [#2811](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2811)
2727
- Introduce the cursor rules to assist project developers [#2820](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2820)
28-
- Search Suggestion Enhancements [#2816](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2890)
28+
- Search Suggestion Enhancements [#2901](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2901)
2929

3030

3131
## v6.1.0 (May 22, 2025)

packages/template-retail-react-app/app/components/search/index.jsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ import debounce from 'lodash/debounce'
3333
import {
3434
RECENT_SEARCH_KEY,
3535
RECENT_SEARCH_LIMIT,
36-
RECENT_SEARCH_MIN_LENGTH,
37-
PRODUCT_BADGE_CUSTOM_FIELD_NAME
36+
RECENT_SEARCH_MIN_LENGTH
3837
} from '@salesforce/retail-react-app/app/constants'
3938
import {
4039
productUrlBuilder,
@@ -82,15 +81,17 @@ const formatSuggestions = (searchSuggestions) => {
8281
link: searchUrlBuilder(brand.phrase)
8382
}
8483
}),
85-
phraseSuggestions: searchSuggestions?.categorySuggestions?.suggestedPhrases?.map(
84+
phraseSuggestions: searchSuggestions?.productSuggestions?.suggestedPhrases?.map(
8685
(phrase) => {
8786
return {
8887
type: 'phrase',
89-
name: capitalize(phrase.phrase),
90-
link: searchUrlBuilder(phrase.phrase)
88+
name: phrase.phrase,
89+
link: searchUrlBuilder(phrase.phrase),
90+
exactMatch: phrase.exactMatch
9191
}
9292
}
93-
)
93+
),
94+
searchPhrase: searchSuggestions?.searchPhrase
9495
}
9596
}
9697

@@ -109,12 +110,12 @@ const Search = (props) => {
109110
const [isOpen, setIsOpen] = useState(false)
110111
const [searchQuery, setSearchQuery] = useState('')
111112
const navigate = useNavigation()
113+
112114
const searchSuggestion = useSearchSuggestions(
113115
{
114116
parameters: {
115117
q: searchQuery,
116-
expand: 'images,prices,custom_product_properties',
117-
includedCustomProductProperties: PRODUCT_BADGE_CUSTOM_FIELD_NAME
118+
expand: 'images,prices'
118119
}
119120
},
120121
{
@@ -266,7 +267,7 @@ const Search = (props) => {
266267
// or we have search suggestions available and have inputed some text (empty text in this scenario should show recent searches)
267268
if (
268269
(document.activeElement.id === 'search-input' && recentSearches?.length > 0) ||
269-
(searchSuggestionsAvailable && searchInputRef.current.value.length > 0)
270+
(searchSuggestionsAvailable && searchInputRef.current?.value?.length > 0)
270271
) {
271272
setIsOpen(true)
272273
} else {
@@ -319,7 +320,15 @@ const Search = (props) => {
319320
</PopoverTrigger>
320321

321322
<HideOnMobile>
322-
<PopoverContent data-testid="sf-suggestion-popover">
323+
<PopoverContent
324+
data-testid="sf-suggestion-popover"
325+
width="100vw"
326+
maxWidth="100vw"
327+
left={0}
328+
right={0}
329+
marginLeft={0}
330+
marginRight={0}
331+
>
323332
<SearchSuggestions
324333
closeAndNavigate={closeAndNavigate}
325334
recentSearches={recentSearches}

packages/template-retail-react-app/app/components/search/index.test.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,99 @@ test('passing undefined to Suggestions returns undefined', async () => {
167167
expect(suggestions.innerHTML).toBeUndefined()
168168
})
169169

170+
test('handles search phrase in formatSuggestions', async () => {
171+
const user = setupUserEvent()
172+
173+
const mockResultsWithPhrase = {
174+
...mockSearchResults,
175+
searchPhrase: 'test search phrase'
176+
}
177+
178+
global.server.use(
179+
rest.get('*/search-suggestions', (req, res, ctx) => {
180+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrase))
181+
})
182+
)
183+
184+
renderWithProviders(<SearchInput />)
185+
const searchInput = document.querySelector('input[type="search"]')
186+
187+
await user.type(searchInput, 'test')
188+
189+
// Wait for suggestions to load with search phrase
190+
await waitFor(() => {
191+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
192+
})
193+
})
194+
195+
test('handles phrase suggestions in formatSuggestions', async () => {
196+
const user = setupUserEvent()
197+
198+
// Mock search results with phrase suggestions
199+
const mockResultsWithPhrases = {
200+
...mockSearchResults,
201+
productSuggestions: {
202+
...mockSearchResults.productSuggestions,
203+
suggestedPhrases: [
204+
{phrase: 'running shoes', exactMatch: true},
205+
{phrase: 'athletic wear', exactMatch: false}
206+
]
207+
}
208+
}
209+
210+
global.server.use(
211+
rest.get('*/search-suggestions', (req, res, ctx) => {
212+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrases))
213+
})
214+
)
215+
216+
renderWithProviders(<SearchInput />)
217+
const searchInput = document.querySelector('input[type="search"]')
218+
219+
await user.type(searchInput, 'running')
220+
221+
// Wait for suggestions to load with phrase suggestions
222+
await waitFor(() => {
223+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
224+
})
225+
})
226+
227+
test('handles product suggestions with images', async () => {
228+
const user = setupUserEvent()
229+
230+
// Mock search results with product suggestions that have images
231+
const mockResultsWithProductImages = {
232+
...mockSearchResults,
233+
productSuggestions: {
234+
...mockSearchResults.productSuggestions,
235+
products: [
236+
{
237+
...mockSearchResults.productSuggestions.products[0],
238+
image: {
239+
disBaseLink: 'https://example.com/product-image.jpg'
240+
}
241+
}
242+
]
243+
}
244+
}
245+
246+
global.server.use(
247+
rest.get('*/search-suggestions', (req, res, ctx) => {
248+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithProductImages))
249+
})
250+
)
251+
252+
renderWithProviders(<SearchInput />)
253+
const searchInput = document.querySelector('input[type="search"]')
254+
255+
await user.type(searchInput, 'Dress')
256+
257+
// Wait for suggestions to load with product images
258+
await waitFor(() => {
259+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
260+
})
261+
})
262+
170263
test('when commerceAgent is disabled, chat functions are not called', async () => {
171264
const user = setupUserEvent()
172265

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2021, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React from 'react'
8+
import PropTypes from 'prop-types'
9+
import {Text, Box, Flex} from '@salesforce/retail-react-app/app/components/shared/ui'
10+
import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image'
11+
import Link from '@salesforce/retail-react-app/app/components/link'
12+
13+
const HorizontalSuggestions = ({suggestions, closeAndNavigate, dynamicImageProps}) => {
14+
if (!suggestions) {
15+
return null
16+
}
17+
18+
return (
19+
<Box data-testid="sf-horizontal-product-suggestions">
20+
<Flex gap="4" overflowX="auto" pb="2">
21+
{suggestions.map((suggestion, idx) => (
22+
<Link
23+
data-testid="product-tile"
24+
to={suggestion.link}
25+
key={idx}
26+
onClick={() => closeAndNavigate(suggestion.link)}
27+
>
28+
<Box>
29+
{/* Product Image */}
30+
<Box position="relative" mb="2" minH="200px">
31+
{suggestion.image && (
32+
<DynamicImage
33+
src={`${suggestion.image}[?sw={width}&q=60]`}
34+
widths={dynamicImageProps?.widths}
35+
imageProps={{
36+
alt: '',
37+
loading: 'eager'
38+
}}
39+
/>
40+
)}
41+
</Box>
42+
43+
<Text
44+
fontSize="sm"
45+
fontWeight="medium"
46+
color="gray.900"
47+
mb="1"
48+
noOfLines={2}
49+
>
50+
{suggestion.name}
51+
</Text>
52+
53+
{suggestion.price && (
54+
<Text fontSize="sm" color="gray.900" fontWeight="medium">
55+
${suggestion.price}
56+
</Text>
57+
)}
58+
</Box>
59+
</Link>
60+
))}
61+
</Flex>
62+
</Box>
63+
)
64+
}
65+
66+
HorizontalSuggestions.propTypes = {
67+
suggestions: PropTypes.array,
68+
closeAndNavigate: PropTypes.func,
69+
dynamicImageProps: PropTypes.object
70+
}
71+
72+
export default HorizontalSuggestions

0 commit comments

Comments
 (0)