-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add auto complete component with comboBox and freeSolo modes (#116
) closes #9
- Loading branch information
1 parent
e5b4286
commit 42b1325
Showing
7 changed files
with
485 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import React, { useRef, useState } from 'react' | ||
import { useOnClickOutside, useOnKeypress } from '../../hooks' | ||
import Paper from '../Paper' | ||
import TextField from '../TextField' | ||
|
||
import Tag from '../Tag' | ||
import * as S from './styles' | ||
|
||
export type MockData = { | ||
id: number | ||
name: string | ||
} | ||
|
||
export type AutoCompleteProps = { | ||
label: string | ||
freeSolo: boolean | ||
fieldPlaceholder: string | ||
options: MockData[] | ||
} | ||
|
||
export const AutoComplete = ({ | ||
label, | ||
freeSolo, | ||
fieldPlaceholder, | ||
options | ||
}: AutoCompleteProps) => { | ||
const ref = useRef<HTMLDivElement>(null) | ||
|
||
const [textFieldValue, setTextFieldValue] = useState('') | ||
const [showOptions, setShowOptions] = useState(false) | ||
const [selectedOption, setSelectedOption] = useState('') | ||
const [filteredSuggestions, setFilteredSuggestions] = useState<MockData[]>([ | ||
...options | ||
]) | ||
|
||
const handleOptionClick = (option: string) => { | ||
setTextFieldValue(option) | ||
setSelectedOption(option) | ||
setShowOptions(false) | ||
setFilteredSuggestions(options) | ||
} | ||
|
||
const optionsFilter = (inputValue: string) => { | ||
return options.filter((option: MockData) => | ||
option.name.toLowerCase().includes(inputValue.toLowerCase()) | ||
) | ||
} | ||
|
||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
showOptions === false ? setShowOptions(true) : null | ||
const currentInputValue = event.target.value | ||
setTextFieldValue(currentInputValue) | ||
const filteredOptions = optionsFilter(currentInputValue) | ||
filteredOptions.length === 0 && freeSolo | ||
? (setShowOptions(false), setFilteredSuggestions(filteredOptions)) | ||
: setFilteredSuggestions(filteredOptions) | ||
currentInputValue === '' && selectedOption !== '' | ||
? setSelectedOption('') | ||
: null | ||
} | ||
|
||
const resetOptions = () => setFilteredSuggestions(options) | ||
|
||
const setTheFields = () => { | ||
freeSolo === false | ||
? selectedOption === '' | ||
? (setTextFieldValue(''), resetOptions()) | ||
: (setTextFieldValue(selectedOption), resetOptions()) | ||
: null | ||
|
||
setShowOptions(false) | ||
} | ||
|
||
useOnClickOutside(ref, () => { | ||
setTheFields() | ||
}) | ||
|
||
useOnKeypress('Escape', () => { | ||
setTheFields() | ||
}) | ||
|
||
const handleOnClick = () => { | ||
if (filteredSuggestions.length === 0) { | ||
setShowOptions(false) | ||
} else if (textFieldValue === '') { | ||
setShowOptions(!showOptions) | ||
} else if (selectedOption !== '') { | ||
setShowOptions(true) | ||
} | ||
} | ||
|
||
return ( | ||
<S.AutoComplete ref={ref} data-testid="auto-complete"> | ||
<> | ||
<TextField | ||
data-testid="auto-complete-input" | ||
label={label} | ||
value={textFieldValue} | ||
placeholder={fieldPlaceholder} | ||
onChange={onInputChange} | ||
onClick={handleOnClick} | ||
style={{ marginRight: 10 }} | ||
/> | ||
{freeSolo === false && ( | ||
<S.AutoCompleteArrow | ||
data-testid="arrow-toggle" | ||
direction={showOptions ? 'up' : 'down'} | ||
show={freeSolo === false ? true : false} | ||
onClick={() => setShowOptions(!showOptions)} | ||
/> | ||
)} | ||
</> | ||
|
||
{showOptions && ( | ||
<Paper data-testid="options" active={showOptions} placement="bottom"> | ||
<div | ||
style={{ | ||
display: 'grid', | ||
gridGap: '0.5rem', | ||
gridTemplateColumns: '1fr', | ||
padding: '0.5rem', | ||
placeItems: 'center' | ||
}} | ||
> | ||
{filteredSuggestions.length > 0 ? ( | ||
filteredSuggestions.map((option) => ( | ||
<Tag | ||
key={option.id} | ||
value={option.name} | ||
onClick={() => handleOptionClick(option.name)} | ||
isPointer | ||
isDarkBgOnHover={true} | ||
isDarkBg={option.name === selectedOption} | ||
/> | ||
)) | ||
) : ( | ||
<S.NoOptionsWrapper>{'No Options'}</S.NoOptionsWrapper> | ||
)} | ||
</div> | ||
</Paper> | ||
)} | ||
</S.AutoComplete> | ||
) | ||
} | ||
|
||
export default AutoComplete |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const mock = [ | ||
{ | ||
id: 1, | ||
name: 'New York' | ||
}, | ||
{ | ||
id: 2, | ||
name: 'Los Angeles' | ||
}, | ||
{ | ||
id: 3, | ||
name: 'Chicago' | ||
}, | ||
{ | ||
id: 4, | ||
name: 'Houston' | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Meta, StoryFn } from '@storybook/react' | ||
import React from 'react' | ||
|
||
import AutoComplete, { AutoCompleteProps } from '.' | ||
import { mock } from './mock' | ||
|
||
export default { | ||
title: 'AutoComplete', | ||
component: AutoComplete, | ||
args: { | ||
label: 'where', | ||
fieldPlaceholder: 'Enter the Input', | ||
freeSolo: false, | ||
options: mock | ||
}, | ||
argTypes: { | ||
fieldValue: { | ||
control: false | ||
} | ||
} | ||
} as Meta | ||
|
||
export const ComboBox: StoryFn<AutoCompleteProps> = (args) => { | ||
return <AutoComplete {...args} freeSolo={false} /> | ||
} | ||
|
||
export const FreeSolo: StoryFn<AutoCompleteProps> = (args) => { | ||
return <AutoComplete {...args} freeSolo /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { BaseHTMLAttributes } from 'react' | ||
import styled, { css } from 'styled-components' | ||
import { Select } from '../Select/styles' | ||
|
||
export type NoOptionsProps = { | ||
size?: 'xsmall' | 'small' | 'medium' | ||
} & BaseHTMLAttributes<HTMLDivElement> | ||
|
||
export const AutoComplete = styled(Select)` | ||
${() => css` | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
`} | ||
` | ||
|
||
export const NoOptionsWrapper = styled.div<Pick<NoOptionsProps, 'size'>>` | ||
${({ theme, size = 'small' }) => css` | ||
padding: ${theme.spacings.mini} ${theme.spacings.xxsmall}; | ||
color: ${theme.colors.neutral.darkest}; | ||
width: fit-content; | ||
height: fit-content; | ||
border-radius: ${theme.border.radius}; | ||
font-size: ${theme.font.sizes[size]}; | ||
font-weight: ${theme.font.weights.regular}; | ||
`} | ||
` | ||
|
||
const figureArrowModifiers = { | ||
up: css` | ||
transform: rotate(-45deg); | ||
`, | ||
down: css` | ||
transform: rotate(135deg); | ||
` | ||
} | ||
|
||
export const AutoCompleteArrow = styled.div<{ | ||
direction: 'up' | 'down' | ||
show: boolean | ||
}>` | ||
${({ theme, direction, show }) => css` | ||
:before { | ||
position: relative; | ||
content: ''; | ||
display: block; | ||
width: 1rem; | ||
height: 1rem; | ||
border-right: 2px solid ${theme.colors.secondary.medium}; | ||
border-top: 2px solid ${theme.colors.secondary.medium}; | ||
visibility: ${!show ? 'hidden' : 'visible'}; | ||
${figureArrowModifiers[direction]} | ||
top: ${direction === 'up' ? '-0.2rem' : 'unset'}; | ||
} | ||
`} | ||
` |
Oops, something went wrong.