mirror of https://github.com/acid-info/lsd.git
Merge pull request #11 from acid-info/topic-implement-autocomplete
Implement autocomplete component
This commit is contained in:
commit
b2e7ecacef
|
@ -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`,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
`
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export * from './Autocomplete'
|
|
@ -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 }> = ({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -9,3 +9,4 @@ export * from './components/Tabs'
|
|||
export * from './components/Theme'
|
||||
export * from './components/Breadcrumb'
|
||||
export * from './components/BreadcrumbItem'
|
||||
export * from './components/Autocomplete'
|
||||
|
|
Loading…
Reference in New Issue