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
27 changes: 27 additions & 0 deletions src/Highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'
import { Typography } from '@material-ui/core'

const escapeHtml = unsafe =>
unsafe
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')

const addHighlight = (query, text, className = '') => {
if (!text) return ''
return escapeHtml(text).replace(
new RegExp(query, 'gi'),
match => `<span class="${className}">${match}</span>`,
)
}

export default function Highlight({ query, text, classes = {}, ...props }) {
return (
<Typography
{...props}
dangerouslySetInnerHTML={{ __html: addHighlight(query, text, classes.highlight) }}
/>
)
}
1 change: 1 addition & 0 deletions src/search/SearchProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default function SearchProvider({ children, query, initialGroups, active
}, 250)

const context = {
query,
state,
setState,
fetchSuggestions,
Expand Down
20 changes: 17 additions & 3 deletions src/search/SearchSuggestionItem.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import React, { useContext } from 'react'
import makeStyles from '@material-ui/core/styles/makeStyles'
import Link from '../link/Link'
import { Typography } from '@material-ui/core'
import PropTypes from 'prop-types'
import Image from '../Image'
import SearchContext from './SearchContext'
import Highlight from '../Highlight'

export const styles = theme => ({
root: {
Expand All @@ -28,6 +29,12 @@ export const styles = theme => ({
},
},
},
text: {},
highlight: {
backgroundColor: 'rgba(0,0,0,0.05)',
borderRadius: '2px',
color: theme.palette.secondary.main,
},
})

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

const { query } = useContext(SearchContext)

return (
<li className={classes.root}>
<Link as={item.as} href={item.href} pageData={item.pageData}>
Expand All @@ -55,7 +64,12 @@ export default function SearchSuggestionItem({
{...thumbnailProps}
{...item.thumbnail}
/>
<Typography>{item.text}</Typography>
<Highlight
className={classes.text}
query={query}
text={item.text}
classes={classes}
/>
</div>
</a>
)}
Expand Down
25 changes: 25 additions & 0 deletions test/Highlight.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
import { mount } from 'enzyme'
import Highlight from 'react-storefront/Highlight'

describe('Highlight', () => {
it('should not blow up if empty props', () => {
const wrapper = mount(<Highlight />)
expect(wrapper.text()).toBe('')
})
it('should not add highlights if no matches', () => {
const wrapper = mount(<Highlight text="the fox jumps over" query="dog" />)
expect(wrapper.text()).toBe('the fox jumps over')
})
it('should escape text', () => {
const wrapper = mount(<Highlight text={`"foo" > 'bar' < zat`} />)
expect(wrapper.text()).toBe('&quot;foo&quot; &gt; &#039;bar&#039; &lt; zat')
})
it('should add highlights to matches', () => {
const wrapper = mount(
<Highlight text="the fox jumps over the ox" query="ox" classes={{ highlight: 'foo' }} />,
)
const matches = wrapper.html().match(/<span class="foo">ox<\/span>/g)
expect(matches.length).toBe(2)
})
})
19 changes: 14 additions & 5 deletions test/search/SearchSuggestionGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import { mount } from 'enzyme'
import SearchSuggestionGroup from 'react-storefront/search/SearchSuggestionGroup'
import SearchSuggestionItem from 'react-storefront/search/SearchSuggestionItem'
import SearchProvider from 'react-storefront/search/SearchProvider'
import PWAContext from 'react-storefront/PWAContext'

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

it('should render children when provided', () => {
wrapper = mount(
<SearchSuggestionGroup caption="">
<div id="child">child</div>
</SearchSuggestionGroup>,
<SearchProvider>
<SearchSuggestionGroup caption="">
<div id="child">child</div>
</SearchSuggestionGroup>
</SearchProvider>,
)

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

expect(wrapper.find(SearchSuggestionItem).length).toBe(2)
})

it('should render provided caption', () => {
wrapper = mount(<SearchSuggestionGroup links={[]} caption="test" />)
wrapper = mount(
<SearchProvider>
<SearchSuggestionGroup links={[]} caption="test" />
</SearchProvider>,
)

expect(wrapper.find(SearchSuggestionGroup).text()).toBe('test')
})
Expand Down
23 changes: 15 additions & 8 deletions test/search/SearchSuggestionItem.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { mount } from 'enzyme'
import SearchProvider from 'react-storefront/search/SearchProvider'
import SearchSuggestionItem from 'react-storefront/search/SearchSuggestionItem'
import Image from 'react-storefront/Image'
import PWAContext from 'react-storefront/PWAContext'
Expand All @@ -14,9 +15,11 @@ describe('SearchSuggestionItem', () => {
it('should render children when provided', () => {
wrapper = mount(
<PWAContext.Provider value={{ hydrating: false }}>
<SearchSuggestionItem item={{ href: '/test' }}>
<div id="child">child</div>
</SearchSuggestionItem>
<SearchProvider>
<SearchSuggestionItem item={{ href: '/test' }}>
<div id="child">child</div>
</SearchSuggestionItem>
</SearchProvider>
</PWAContext.Provider>,
)

Expand All @@ -26,7 +29,9 @@ describe('SearchSuggestionItem', () => {
it('should render image with a text when no children provided', () => {
wrapper = mount(
<PWAContext.Provider value={{ hydrating: false }}>
<SearchSuggestionItem item={{ href: '/test', text: 'test' }} />
<SearchProvider>
<SearchSuggestionItem item={{ href: '/test', text: 'test' }} />
</SearchProvider>
</PWAContext.Provider>,
)

Expand All @@ -42,10 +47,12 @@ describe('SearchSuggestionItem', () => {
it('should spread thumbnail props on image', () => {
wrapper = mount(
<PWAContext.Provider value={{ hydrating: false }}>
<SearchSuggestionItem
item={{ href: '/test', thumbnail: { testprop2: 'test2' } }}
thumbnailProps={{ testprop1: 'test1' }}
/>
<SearchProvider>
<SearchSuggestionItem
item={{ href: '/test', thumbnail: { testprop2: 'test2' } }}
thumbnailProps={{ testprop1: 'test1' }}
/>
</SearchProvider>
</PWAContext.Provider>,
)

Expand Down