Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesigns autocomplete component and adds metric tracking #11

Merged
merged 7 commits into from
Apr 5, 2022
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lob/react-address-autocomplete",
"version": "1.1.5",
"version": "1.1.7",
"description": "A collection of components and utility functions for verifying and suggesting addresses using Lob",
"author": "Lob",
"license": "MIT",
Expand Down
17 changes: 9 additions & 8 deletions src/AddressForm/AddressForm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ describe('AddressForm', () => {
})

// Verify suggestion renderings
expect(screen.getByText('123 Sesame St New York NY')).toBeVisible()
expect(
screen.getByText("123 Bowser's Castle Mushroom Kingdom JA")
).toBeVisible()
expect(
screen.getByText("123 Micky's Clubhouse Disneyland FL")
).toBeVisible()
expect(screen.getAllByText('123')).toHaveLength(4)
expect(screen.getByText('Sesame St,')).toBeVisible()
expect(screen.getByText('New York, NY, 12345')).toBeVisible()

expect(screen.getByText("Bowser's Castle,")).toBeVisible()
expect(screen.getByText('Mushroom Kingdom, JA, 12345')).toBeVisible()
expect(screen.getByText("Micky's Clubhouse,")).toBeVisible()
expect(screen.getByText('Disneyland, FL, 12345')).toBeVisible()

// Simulate selection
await act(async () => {
const suggestion = screen.getByText('123 Sesame St New York NY')
const suggestion = screen.getByText('Sesame St,')
await fireEvent.click(suggestion)
})

Expand Down
36 changes: 36 additions & 0 deletions src/Autocomplete.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.lob-gray-text {
color: #888;
text-decoration: inherit;
}

.lob-label {
align-items: center;
border-bottom: 1px solid #DDDDDD;
cursor: pointer;
display: flex;
font-size: 17px;
padding: 16px;
pointer-events: none;
}

.lob-label > a {
font-weight: 600;
color: #0699D6;
text-decoration: inherit;
}

.lob-label > span {
flex: 1;
font-weight: 400;
margin-left: 12px;
}

.lob-logo {
height: .9em;
margin-left: 1px;
margin-top: 3px;
}

.logo-large {
height: 21px;
}
143 changes: 122 additions & 21 deletions src/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,88 @@
import React, { useEffect, useState, useRef } from 'react'
import Select, { components } from 'react-select'
import throttle from 'lodash.throttle'
import './Autocomplete.css'

// Internal Dependencies
import { postAutocompleteAddress } from './api'

const getLobLabel = () => (
<a
href='https://www.lob.com/address-verification'
style={{ color: 'hsl(0, 0%, 50%)', textDecoration: 'inherit' }}
>
<span style={{ verticalAlign: 'top' }}>Powered by </span>
const LOB_LABEL = 'lob-label'
const LOB_URL =
'https://www.lob.com/address-verification?utm_source=autocomplete&utm_medium=react'

const LobLogo = ({ className }) => {
return (
<svg
style={{ height: '.9em', marginLeft: '1px', marginTop: '3px' }}
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 1259 602'
className={className}
>
<path
fill='#0099d7'
/* eslint-ignore-next-line */
d='M1063,141c-47.06,0-89,18.33-121,50.78V0H780V338.74C765,222.53,666.88,138,540,138c-137,0-242,101-242,232a235,235,0,0,0,7.7,60H164V0H0V585H307l14.54-112.68C359.94,550,441.74,602,540,602c127.75,0,225.08-83.62,240-200.41V585H930V540.27c31.8,37,77.27,56.73,133,56.73,103,0,196-109,196-228C1259,239,1175,141,1063,141ZM540,450c-45,0-81-36-81-80s36-80,81-80c46,0,81,35,81,80S585,450,540,450Zm475-1c-46,0-83-36-83-80a82.8,82.8,0,0,1,82.6-83h.4c47,0,85,37,85,83C1100,413,1062,449,1015,449Z'
/>
</svg>
)
}

const poweredByLob = () => (
<a href={LOB_URL} className='lob-gray-text'>
<span style={{ verticalAlign: 'top' }}>Powered by </span>
<LobLogo className='lob-logo' />
</a>
)

const getLobLabel = () => (
<div className={LOB_LABEL}>
<LobLogo className='logo-large' />
<span className='lob-gray-text'>Deliverable addresses</span>
<a href={LOB_URL}>Learn more</a>
</div>
)

// Highlight the users input in the primary line by comparing char by char. We only check the
// primary line for simplicity sake
const getOptionElement = (suggestion, inputValue) => {
/* eslint-disable camelcase */
const { primary_line, city, state, zip_code } = suggestion

let boldStopIndex = 0

inputValue.split('').forEach((inputChar) => {
if (
inputChar.toLowerCase() ===
primary_line.charAt(boldStopIndex).toLowerCase()
) {
boldStopIndex += 1
}
})

const primaryLineElement =
boldStopIndex === 0 ? (
<span>{primary_line}, </span>
) : boldStopIndex === primary_line.length ? (
<span>
<strong>{primary_line}, </strong>
</span>
) : (
<span>
<strong>{primary_line.substring(0, boldStopIndex)}</strong>
{primary_line.substring(boldStopIndex)},{' '}
</span>
)

return (
<span>
{primaryLineElement}
<span className='lob-gray-text'>
{city},&nbsp;{state.toUpperCase()},&nbsp;{zip_code}
</span>
</span>
)
/* eslint-enable camelcase */
}

/**
* Part of Lob's response body schema for US autocompletions
* https://docs.lob.com/#section/Autocompletion-Test-Env
Expand Down Expand Up @@ -115,15 +173,15 @@ const Autocomplete = ({

const newSuggestions = suggestions.map((x) => ({
value: x,
label: `${x.primary_line} ${x.city} ${x.state}`
label: getOptionElement(x, inputValue)
}))

setAutocompleteResults([
...newSuggestions,
{
value: 'none',
value: LOB_LABEL,
label: getLobLabel()
}
},
...newSuggestions
])
})
.catch((err) => {
Expand All @@ -143,9 +201,28 @@ const Autocomplete = ({
fetchData(inputValue, addressComponentValues)
}
}
}, [inputValue])
}, [inputValue, delaySearch])

/** Event handlers */
const updateInputValueFromOption = (option) => {
if (!option) {
setInputValue('')
return
}

/* eslint-disable camelcase */
const { primary_line, secondary_line, city, state, zip_code } = option.value

if (primaryLineOnly) {
setInputValue(primary_line)
} else {
const secondary = secondary_line ? ' ' + secondary_line : ''
setInputValue(
`${primary_line}${secondary}, ${city}, ${state}, ${zip_code}`
)
}
/* eslint-enable camelcase */
}

// Fire when the user types into the input
const handleInputChange = (newInputValue, { action }) => {
Expand All @@ -163,6 +240,11 @@ const Autocomplete = ({

// Fires when the select component has changed (as opposed to the input inside the select)
const handleChange = (option) => {
if (option.value === LOB_LABEL) {
window.location.href = LOB_URL
return
}

// User has pasted an address directly into input, let's call the API
if (typeof option === 'string') {
setInputValue(option)
Expand All @@ -171,35 +253,54 @@ const Autocomplete = ({
return
}

if (primaryLineOnly) {
setInputValue(option ? option.value.primary_line : '')
} else {
setInputValue(option ? option.label : '')
}

setSelectValue(option)
updateInputValueFromOption(option)
onSelection(option)
}

const handleSelect = (option) => {
if (option.value !== LOB_LABEL) {
updateInputValueFromOption(option)
onSelection(option)
}
}

const customFilter = (candidate, input) => {
return candidate
}

// Remove padding from first option which is our Lob label
const customStyles = {
option: (styles, { data }) => {
if (data.value === LOB_LABEL) {
return {
...styles,
background: 'none',
cursor: 'pointer',
padding: '0'
}
}
return styles
},
...reactSelectProps.styles
}

return (
<Select
components={{ Input }}
inputValue={inputValue}
options={autocompleteResults}
controlShouldRenderValue={false}
noOptionsMessage={getLobLabel}
noOptionsMessage={poweredByLob}
placeholder='Start typing an address...'
value={selectValue}
{...reactSelectProps}
// We don't let user completely override onChange and onInputChange and risk them breaking
// the behavior of our input component.
filterOption={customFilter}
onChange={handleChange}
onInputChange={handleInputChange}
filterOption={customFilter}
onSelect={handleSelect}
styles={customStyles}
/>
)
}
Expand Down
46 changes: 23 additions & 23 deletions src/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,25 @@ describe('Autocomplete', () => {
})

// Verify suggestion renderings
expect(screen.getByText('123 Sesame St New York NY')).toBeVisible()
expect(
screen.getByText("123 Bowser's Castle Mushroom Kingdom JA")
).toBeVisible()
expect(
screen.getByText("123 Micky's Clubhouse Disneyland FL")
).toBeVisible()
expect(screen.getAllByText('123')).toHaveLength(4)
expect(screen.getByText('Sesame St,')).toBeVisible()
expect(screen.getByText('New York, NY, 12345')).toBeVisible()

expect(screen.getByText("Bowser's Castle,")).toBeVisible()
expect(screen.getByText('Mushroom Kingdom, JA, 12345')).toBeVisible()
expect(screen.getByText("Micky's Clubhouse,")).toBeVisible()
expect(screen.getByText('Disneyland, FL, 12345')).toBeVisible()

// Simulate selection
await act(async () => {
const suggestion = screen.getByText('123 Sesame St New York NY')
const suggestion = screen.getByText('Sesame St,')
await fireEvent.click(suggestion)
})

// Verify correct suggestion is rendered
// expect(screen.getByText('123 Sesame St New York NY')).toBeVisible()
expect(screen.queryByText("123 Bowser's Castle Mushroom Kingdom JA")).toBe(
null
)
expect(screen.queryByText("123 Micky's Clubhouse Disneyland FL")).toBe(null)
expect(screen.getByText('123 Sesame St, New York, NY, 12345')).toBeDefined()
expect(screen.queryByText("Bowser's Castle")).toBe(null)
expect(screen.queryByText("Micky's Clubhouse")).toBe(null)
})

it('fires callback functions as expected', async () => {
Expand Down Expand Up @@ -86,20 +85,21 @@ describe('Autocomplete', () => {

// Trigger selection
await act(async () => {
const suggestion = screen.getByText('123 Sesame St New York NY')
const suggestion = screen.getByText('Sesame St,')
await fireEvent.click(suggestion)
})

expect(handleSelection).toHaveBeenCalledTimes(1)
expect(handleSelection).toHaveBeenCalledWith({
label: '123 Sesame St New York NY',
value: {
city: 'New York',
primary_line: '123 Sesame St',
state: 'NY',
zip_code: '12345'
}
})
expect(handleSelection).toHaveBeenCalledWith(
expect.objectContaining({
value: {
city: 'New York',
primary_line: '123 Sesame St',
state: 'NY',
zip_code: '12345'
}
})
)
})

it('handles errors as expected', async () => {
Expand Down
Loading