Skip to content

Commit b2fddca

Browse files
authored
Added highlight to search suggestions (#117)
* Added highlight to search suggestions * Using JSS class for highlight instead * Added highlight tests
1 parent 0e22335 commit b2fddca

File tree

6 files changed

+99
-16
lines changed

6 files changed

+99
-16
lines changed

src/Highlight.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react'
2+
import { Typography } from '@material-ui/core'
3+
4+
const escapeHtml = unsafe =>
5+
unsafe
6+
.replace(/&/g, '&')
7+
.replace(/</g, '&lt;')
8+
.replace(/>/g, '&gt;')
9+
.replace(/"/g, '&quot;')
10+
.replace(/'/g, '&#039;')
11+
12+
const addHighlight = (query, text, className = '') => {
13+
if (!text) return ''
14+
return escapeHtml(text).replace(
15+
new RegExp(query, 'gi'),
16+
match => `<span class="${className}">${match}</span>`,
17+
)
18+
}
19+
20+
export default function Highlight({ query, text, classes = {}, ...props }) {
21+
return (
22+
<Typography
23+
{...props}
24+
dangerouslySetInnerHTML={{ __html: addHighlight(query, text, classes.highlight) }}
25+
/>
26+
)
27+
}

src/search/SearchProvider.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default function SearchProvider({ children, query, initialGroups, active
4646
}, 250)
4747

4848
const context = {
49+
query,
4950
state,
5051
setState,
5152
fetchSuggestions,

src/search/SearchSuggestionItem.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React from 'react'
1+
import React, { useContext } from 'react'
22
import makeStyles from '@material-ui/core/styles/makeStyles'
33
import Link from '../link/Link'
4-
import { Typography } from '@material-ui/core'
54
import PropTypes from 'prop-types'
65
import Image from '../Image'
6+
import SearchContext from './SearchContext'
7+
import Highlight from '../Highlight'
78

89
export const styles = theme => ({
910
root: {
@@ -28,6 +29,12 @@ export const styles = theme => ({
2829
},
2930
},
3031
},
32+
text: {},
33+
highlight: {
34+
backgroundColor: 'rgba(0,0,0,0.05)',
35+
borderRadius: '2px',
36+
color: theme.palette.secondary.main,
37+
},
3138
})
3239

3340
const useStyles = makeStyles(styles, { name: 'RSFSearchSuggestionItem' })
@@ -42,6 +49,8 @@ export default function SearchSuggestionItem({
4249
}) {
4350
classes = useStyles({ classes })
4451

52+
const { query } = useContext(SearchContext)
53+
4554
return (
4655
<li className={classes.root}>
4756
<Link as={item.as} href={item.href} pageData={item.pageData}>
@@ -55,7 +64,12 @@ export default function SearchSuggestionItem({
5564
{...thumbnailProps}
5665
{...item.thumbnail}
5766
/>
58-
<Typography>{item.text}</Typography>
67+
<Highlight
68+
className={classes.text}
69+
query={query}
70+
text={item.text}
71+
classes={classes}
72+
/>
5973
</div>
6074
</a>
6175
)}

test/Highlight.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import { mount } from 'enzyme'
3+
import Highlight from 'react-storefront/Highlight'
4+
5+
describe('Highlight', () => {
6+
it('should not blow up if empty props', () => {
7+
const wrapper = mount(<Highlight />)
8+
expect(wrapper.text()).toBe('')
9+
})
10+
it('should not add highlights if no matches', () => {
11+
const wrapper = mount(<Highlight text="the fox jumps over" query="dog" />)
12+
expect(wrapper.text()).toBe('the fox jumps over')
13+
})
14+
it('should escape text', () => {
15+
const wrapper = mount(<Highlight text={`"foo" > 'bar' < zat`} />)
16+
expect(wrapper.text()).toBe('&quot;foo&quot; &gt; &#039;bar&#039; &lt; zat')
17+
})
18+
it('should add highlights to matches', () => {
19+
const wrapper = mount(
20+
<Highlight text="the fox jumps over the ox" query="ox" classes={{ highlight: 'foo' }} />,
21+
)
22+
const matches = wrapper.html().match(/<span class="foo">ox<\/span>/g)
23+
expect(matches.length).toBe(2)
24+
})
25+
})

test/search/SearchSuggestionGroup.test.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22
import { mount } from 'enzyme'
33
import SearchSuggestionGroup from 'react-storefront/search/SearchSuggestionGroup'
44
import SearchSuggestionItem from 'react-storefront/search/SearchSuggestionItem'
5+
import SearchProvider from 'react-storefront/search/SearchProvider'
56
import PWAContext from 'react-storefront/PWAContext'
67

78
describe('SearchSuggestionGroup', () => {
@@ -13,9 +14,11 @@ describe('SearchSuggestionGroup', () => {
1314

1415
it('should render children when provided', () => {
1516
wrapper = mount(
16-
<SearchSuggestionGroup caption="">
17-
<div id="child">child</div>
18-
</SearchSuggestionGroup>,
17+
<SearchProvider>
18+
<SearchSuggestionGroup caption="">
19+
<div id="child">child</div>
20+
</SearchSuggestionGroup>
21+
</SearchProvider>,
1922
)
2023

2124
expect(wrapper.find('#child').text()).toBe('child')
@@ -24,15 +27,21 @@ describe('SearchSuggestionGroup', () => {
2427
it('should render suggested items when no children provided', () => {
2528
wrapper = mount(
2629
<PWAContext.Provider value={{ hydrating: false }}>
27-
<SearchSuggestionGroup caption="" links={[{ href: '/test1' }, { href: '/test2' }]} />
30+
<SearchProvider>
31+
<SearchSuggestionGroup caption="" links={[{ href: '/test1' }, { href: '/test2' }]} />
32+
</SearchProvider>
2833
</PWAContext.Provider>,
2934
)
3035

3136
expect(wrapper.find(SearchSuggestionItem).length).toBe(2)
3237
})
3338

3439
it('should render provided caption', () => {
35-
wrapper = mount(<SearchSuggestionGroup links={[]} caption="test" />)
40+
wrapper = mount(
41+
<SearchProvider>
42+
<SearchSuggestionGroup links={[]} caption="test" />
43+
</SearchProvider>,
44+
)
3645

3746
expect(wrapper.find(SearchSuggestionGroup).text()).toBe('test')
3847
})

test/search/SearchSuggestionItem.test.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import { mount } from 'enzyme'
3+
import SearchProvider from 'react-storefront/search/SearchProvider'
34
import SearchSuggestionItem from 'react-storefront/search/SearchSuggestionItem'
45
import Image from 'react-storefront/Image'
56
import PWAContext from 'react-storefront/PWAContext'
@@ -14,9 +15,11 @@ describe('SearchSuggestionItem', () => {
1415
it('should render children when provided', () => {
1516
wrapper = mount(
1617
<PWAContext.Provider value={{ hydrating: false }}>
17-
<SearchSuggestionItem item={{ href: '/test' }}>
18-
<div id="child">child</div>
19-
</SearchSuggestionItem>
18+
<SearchProvider>
19+
<SearchSuggestionItem item={{ href: '/test' }}>
20+
<div id="child">child</div>
21+
</SearchSuggestionItem>
22+
</SearchProvider>
2023
</PWAContext.Provider>,
2124
)
2225

@@ -26,7 +29,9 @@ describe('SearchSuggestionItem', () => {
2629
it('should render image with a text when no children provided', () => {
2730
wrapper = mount(
2831
<PWAContext.Provider value={{ hydrating: false }}>
29-
<SearchSuggestionItem item={{ href: '/test', text: 'test' }} />
32+
<SearchProvider>
33+
<SearchSuggestionItem item={{ href: '/test', text: 'test' }} />
34+
</SearchProvider>
3035
</PWAContext.Provider>,
3136
)
3237

@@ -42,10 +47,12 @@ describe('SearchSuggestionItem', () => {
4247
it('should spread thumbnail props on image', () => {
4348
wrapper = mount(
4449
<PWAContext.Provider value={{ hydrating: false }}>
45-
<SearchSuggestionItem
46-
item={{ href: '/test', thumbnail: { testprop2: 'test2' } }}
47-
thumbnailProps={{ testprop1: 'test1' }}
48-
/>
50+
<SearchProvider>
51+
<SearchSuggestionItem
52+
item={{ href: '/test', thumbnail: { testprop2: 'test2' } }}
53+
thumbnailProps={{ testprop1: 'test1' }}
54+
/>
55+
</SearchProvider>
4956
</PWAContext.Provider>,
5057
)
5158

0 commit comments

Comments
 (0)