Merge pull request #11 from acid-info/topic-implement-autocomplete

Implement autocomplete component
This commit is contained in:
jeangovil 2023-02-23 15:25:53 +03:30 committed by GitHub
commit b2e7ecacef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 334 additions and 35 deletions

View File

@ -0,0 +1,17 @@
export const autocompleteClasses = {
root: `lsd-autocomplete`,
input: `lsd-autocomplete__input`,
icon: `lsd-autocomplete__icon`,
listBox: `lsd-autocomplete-list-box`,
dropdownItem: `lsd-autocomplete-dropdown-item`,
dropdownItemPlaceholder: `lsd-autocomplete-dropdown-item__placeholder`,
disabled: `lsd-autocomplete--disabled`,
error: 'lsd-autocomplete--error',
large: `lsd-autocomplete--large`,
medium: `lsd-autocomplete--medium`,
withIcon: `lsd-autocomplete--with-icon`,
}

View File

@ -0,0 +1,33 @@
import { Meta, Story } from '@storybook/react'
import { Autocomplete, AutocompleteProps } from './Autocomplete'
const list = JSON.parse(
'["BitTorrent","VANIG","BenjiRolls","Status Network Token","GigTricks","Vechain","Insureum","Instant Sponsor Token","IslaCoin","Kcash","Litecoin","BoutsPro","Valorem","Artcoin","Insureum","Zen Protocol","CryptoPennies","MediBond","WishFinance","BlockNet","Big Data Block","Loom Network","Gamedex","Universal Recognition Token","Soarcoin","IZX","aelf","Bitcoin Private","TerraCoin","Coinnec","Revolution VR","SecureCoin","Swarm Fund","Fan360","LakeBanker","16BitCoin","CyberTrust","Actinium","ZestCoin","Monero 0","DACash","Bitswift","Bethereum","Hedge Token","Megastake","HeelCoin","RealChain","LiteBitcoin","SexCoin","Suretly","Internet of People","Rate3","Invictus","WorldPay","iOlite","Cashaa","NovaCoin","U Network","Gimli","Chynge.net","BMChain","Ethereum Premium","BlackShadowCoin","Javvy","Befund","Bancor Network Token","BOONSCoin","Pantos","IDEX Membership","BitStation","Lynx","Encrybit","HighVibe.Network","Credo","HiCoin","RiptideCoin","BitBoss","NXTTY","Presale Ventures","Urbit Data","Xeonbit","Newbium","Mint","Crypto Wine Exchange","HARA","Pioneer Coin","Ethereum Dark","FazzCoin","Fitrova","The EFFECT Network","CargoX","PolicyPal Network","Vechain","President Clinton","GenesysCoin","Trivver","Bitdeal","ShareMeAll","Primas","RoboAdvisorCoin","Liquid","Napoleon X","NOKU Master token","Liqnet","ZeroState","0chain","DarkCash","Sudan Gold Coin","PokerSports","Bankera","Think And Get Rich Coin","ELTCOIN","USOAMIC","XDNA","Autoria","Cosmo","Bigbom","EagsCoin","Stakinglab","SaffronCoin","BnrtxCoin","FoodCoin","PutinCoin","SID Token","Quartz","IOU1","Spend","NEO Gold","High Voltage Coin","Unobtanium","Sandcoin","FrazCoin","Sudan Gold Coin","VeriCoin","Aurora","NANJCOIN","Muse","DuckDuckCoin","Saifu","CryCash","Rustbits","BitFlip","Cpollo","Monkey Project","Coin Analyst","Scanetchain Token","Aditus","LendConnect","FOREXCOIN","Crypto Improvement Fund","Electra","Psilocybin","GameLeagueCoin","Switcheo","BitQuark","BinaryCoin","CyberVein","KATZcoin","BtcEX","ByteCoin","Shping Coin","Liquid","Stacktical","Tezos","ParkByte","Imbrex","Pinmo","Sigil","LePenCoin","OmiseGO Classic","Digix DAO","ShareRing","CryptoCarbon","CDX Network","SONM","PhoenixCoin","Incent","Tokyo Coin","Premium","Unattanium","Biotron","WETH","Rublebit","KRCoin","Aegis","Legends Cryptocurrency","VARcrypt","Witcoin","GlowShares","HOQU","VARcrypt","Linx","BlackholeCoin","NumbersCoin","ZayedCoin","CarVertical","Securosys","ElliotCoin","Zelcash","AcesCoin","EtherInc"]',
)
export default {
title: 'Autocomplete',
component: Autocomplete,
argTypes: {
size: {
type: {
name: 'enum',
value: ['medium', 'large'],
},
defaultValue: 'large',
},
},
} as Meta
export const Root: Story<AutocompleteProps> = (args) => (
<Autocomplete {...args}>Autocomplete</Autocomplete>
)
Root.args = {
size: 'large',
disabled: false,
withIcon: false,
error: false,
placeholder: 'Placeholder',
onChange: undefined,
options: list,
}

View File

@ -0,0 +1,83 @@
import { css } from '@emotion/react'
import { autocompleteClasses } from './Autocomplete.classes'
export const AutocompleteStyles = css`
.${autocompleteClasses.root} {
width: auto;
border: 1px solid rgb(var(--lsd-border-primary));
box-sizing: border-box;
align-items: center;
}
.${autocompleteClasses.root} > div {
display: flex;
justify-content: space-between;
}
.${autocompleteClasses.disabled} {
opacity: 0.34;
}
.${autocompleteClasses.input} {
border: none;
outline: none;
font-size: 14px;
color: rgb(var(--lsd-text-primary));
background: none;
width: 100%;
}
.${autocompleteClasses.input}:hover {
outline: none;
}
.${autocompleteClasses.input}::placeholder {
color: rgb(var(--lsd-text-primary));
opacity: 0.3;
}
.${autocompleteClasses.error} {
text-decoration: line-through;
}
.${autocompleteClasses.large} {
width: 208px;
height: 40px;
padding: 10px 14px;
}
.${autocompleteClasses.medium} {
width: 188px;
height: 32px;
padding: 6px 12px;
}
.${autocompleteClasses.withIcon} {
}
.${autocompleteClasses.icon} {
cursor: pointer;
display: flex;
align-items: center;
}
.${autocompleteClasses.listBox} {
max-height: 400px;
overflow: auto;
border: 1px solid rgb(var(--lsd-border-primary));
border-top: 0;
}
.${autocompleteClasses.dropdownItem} {
border: 0;
&:not(:last-child) {
border-bottom: 1px solid rgb(var(--lsd-border-primary));
}
}
.${autocompleteClasses.dropdownItemPlaceholder} {
opacity: 0.5;
white-space: pre;
}
`

View File

@ -0,0 +1,150 @@
import clsx from 'clsx'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useInput } from '../../utils/useInput'
import { DropdownItem } from '../DropdownItem'
import { CloseIcon, SearchIcon } from '../Icons'
import { ListBox } from '../ListBox'
import { Portal } from '../PortalProvider/Portal'
import { autocompleteClasses } from './Autocomplete.classes'
export type AutocompleteProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'onChange' | 'value'
> &
Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
size?: 'large' | 'medium'
withIcon?: boolean
error?: boolean
disabled?: boolean
placeholder?: string
value?: string
defaultValue?: string
options?: string[]
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}
export const Autocomplete: React.FC<AutocompleteProps> & {
classes: typeof autocompleteClasses
} = ({
size = 'large',
withIcon = false,
error = false,
disabled = false,
children,
value,
defaultValue,
placeholder,
onChange,
options = [],
inputProps = {},
...props
}) => {
const ref = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const input = useInput({ defaultValue, value, onChange, ref })
const inputValue = input.value as string
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<string>()
const onCancel = () => input.setValue('')
const handleDropdownClick = (value: string) => {
setOpen(false)
setSelected(value)
input.setValue(value)
}
const suggestions = useMemo(
() =>
input.filled
? options
.filter((option) =>
new RegExp(`^${input.value}.+`, 'i').test(option),
)
.map((option) => [
option,
option.slice(0, inputValue.length),
option.slice(inputValue.length),
])
: options,
[input.value, options],
)
useEffect(() => {
!selected && input.filled && !open && setOpen(true)
}, [input.value, selected, open])
const isOpen = !disabled && open && suggestions.length > 0 && input.filled
return (
<div
ref={containerRef}
className={clsx(
props.className,
autocompleteClasses.root,
autocompleteClasses[size],
disabled && autocompleteClasses.disabled,
withIcon && autocompleteClasses.withIcon,
)}
{...props}
>
<div>
<input
{...inputProps}
ref={ref}
value={input.value}
placeholder={placeholder}
onChange={input.onChange}
disabled={disabled}
onFocus={() => setOpen(true)}
className={clsx(
inputProps.className,
autocompleteClasses.input,
error && autocompleteClasses.error,
)}
/>
{withIcon && input.value ? (
<span className={autocompleteClasses.icon} onClick={onCancel}>
<CloseIcon color="primary" />
</span>
) : withIcon && !input.value ? (
<span className={autocompleteClasses.icon}>
<SearchIcon color="primary" />
</span>
) : null}
</div>
<Portal id="autocomplete">
<ListBox
handleRef={containerRef}
open={isOpen}
onClose={() => setOpen(false)}
className={autocompleteClasses.listBox}
>
{suggestions.map((opt, idx: number) => (
<DropdownItem
key={idx}
size={size}
tabIndex={0}
label={
<>
{opt[1]}
<span className={autocompleteClasses.dropdownItemPlaceholder}>
{opt[2]}
</span>
</>
}
className={autocompleteClasses.dropdownItem}
onClick={() => handleDropdownClick(opt[0])}
onKeyDown={(e) =>
e.key === 'Enter' && handleDropdownClick(opt[0])
}
/>
))}
</ListBox>
</Portal>
</div>
)
}
Autocomplete.classes = autocompleteClasses

View File

@ -0,0 +1 @@
export * from './Autocomplete'

View File

@ -13,6 +13,7 @@ import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles'
import { defaultThemes, Theme, withTheme } from '../Theme'
import { TypographyStyles } from '../Typography/Typography.styles'
import { TextFieldStyles } from '../TextField/TextField.styles'
import { AutocompleteStyles } from '../Autocomplete/Autocomplete.styles'
const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
[
@ -28,6 +29,7 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
BreadcrumbStyles,
BreadcrumbItemStyles,
TextFieldStyles,
AutocompleteStyles,
]
export const CSSBaseline: React.FC<{ theme?: Theme }> = ({

View File

@ -4,8 +4,11 @@ import { CheckboxFilledIcon, CheckboxIcon, LsdIconProps } from '../Icons'
import { Typography } from '../Typography'
import { dropdownItemClasses } from './DropdownItem.classes'
export type DropdownItemProps = React.HTMLAttributes<HTMLDivElement> & {
label: string
export type DropdownItemProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'label'
> & {
label: React.ReactNode
selected?: boolean
withIcon?: boolean
disabled?: boolean
@ -32,7 +35,6 @@ export const DropdownItem: React.FC<DropdownItemProps> & {
<div
role="option"
aria-selected={selected ? 'true' : 'false'}
aria-label={label}
className={clsx(
className,
dropdownItemClasses.root,

View File

@ -1,17 +1,22 @@
import { LsdIcon } from '../LsdIcon'
export const CloseIcon = LsdIcon((props) => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11.0834 3.73916L10.2609 2.91666L7.00008 6.17749L3.73925 2.91666L2.91675 3.73916L6.17758 6.99999L2.91675 10.2608L3.73925 11.0833L7.00008 7.82249L10.2609 11.0833L11.0834 10.2608L7.82258 6.99999L11.0834 3.73916Z"
fill="black"
/>
</svg>
))
export const CloseIcon = LsdIcon(
(props) => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11.0834 3.73916L10.2609 2.91666L7.00008 6.17749L3.73925 2.91666L2.91675 3.73916L6.17758 6.99999L2.91675 10.2608L3.73925 11.0833L7.00008 7.82249L10.2609 11.0833L11.0834 10.2608L7.82258 6.99999L11.0834 3.73916Z"
fill="black"
/>
</svg>
),
{
filled: true,
},
)

View File

@ -1,19 +1,24 @@
import { LsdIcon } from '../LsdIcon'
export const SearchIcon = LsdIcon((props) => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.61 7.74083L11.9525 11.0833L11.0833 11.9525L7.74083 8.61C7.11667 9.05917 6.36417 9.33333 5.54167 9.33333C3.4475 9.33333 1.75 7.63583 1.75 5.54167C1.75 3.4475 3.4475 1.75 5.54167 1.75C7.63583 1.75 9.33333 3.4475 9.33333 5.54167C9.33333 6.36417 9.05917 7.11667 8.61 7.74083ZM5.54167 2.91667C4.08917 2.91667 2.91667 4.08917 2.91667 5.54167C2.91667 6.99417 4.08917 8.16667 5.54167 8.16667C6.99417 8.16667 8.16667 6.99417 8.16667 5.54167C8.16667 4.08917 6.99417 2.91667 5.54167 2.91667Z"
fill="black"
/>
</svg>
))
export const SearchIcon = LsdIcon(
(props) => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.61 7.74083L11.9525 11.0833L11.0833 11.9525L7.74083 8.61C7.11667 9.05917 6.36417 9.33333 5.54167 9.33333C3.4475 9.33333 1.75 7.63583 1.75 5.54167C1.75 3.4475 3.4475 1.75 5.54167 1.75C7.63583 1.75 9.33333 3.4475 9.33333 5.54167C9.33333 6.36417 9.05917 7.11667 8.61 7.74083ZM5.54167 2.91667C4.08917 2.91667 2.91667 4.08917 2.91667 5.54167C2.91667 6.99417 4.08917 8.16667 5.54167 8.16667C6.99417 8.16667 8.16667 6.99417 8.16667 5.54167C8.16667 4.08917 6.99417 2.91667 5.54167 2.91667Z"
fill="black"
/>
</svg>
),
{
filled: true,
},
)

View File

@ -9,3 +9,4 @@ export * from './components/Tabs'
export * from './components/Theme'
export * from './components/Breadcrumb'
export * from './components/BreadcrumbItem'
export * from './components/Autocomplete'