handle search type for text field

This commit is contained in:
2024-05-12 13:17:47 +02:00
parent 38e734523c
commit b8430e2c74
8 changed files with 6992 additions and 5482 deletions

View File

@@ -1,3 +1,6 @@
module.exports = { module.exports = {
extends: '@it-incubator/stylelint-config', extends: '@it-incubator/stylelint-config',
rules: {
'value-keyword-case': null,
}
} }

12347
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -89,9 +89,17 @@
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0.25rem;
outline: 0; outline: 0;
&:focus-visible { &:focus-visible {
outline: var(--outline-focus); outline: var(--outline-focus);
} }
} }
.clearInput {
composes: showPassword;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,7 +1,16 @@
import { ChangeEvent, ComponentProps, ComponentPropsWithoutRef, forwardRef, useState } from 'react' import {
ChangeEvent,
ComponentProps,
ComponentPropsWithoutRef,
forwardRef,
useId,
useRef,
useState,
} from 'react'
import { Eye, Search, VisibilityOff } from '@/assets' import { Close, Eye, Search, VisibilityOff } from '@/assets'
import { Typography } from '@/components' import { Typography } from '@/components'
import { mergeRefs } from '@/utils'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import s from './text-field.module.scss' import s from './text-field.module.scss'
@@ -11,8 +20,12 @@ export type TextFieldProps = {
errorMessage?: string errorMessage?: string
label?: string label?: string
labelProps?: ComponentProps<'label'> labelProps?: ComponentProps<'label'>
/**
* Callback that is called when the clear button is clicked
* If not provided clears the internal value via ref and calls onValueChange with an empty string
*/
onClearInput?: () => void
onValueChange?: (value: string) => void onValueChange?: (value: string) => void
search?: boolean
} & ComponentPropsWithoutRef<'input'> } & ComponentPropsWithoutRef<'input'>
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>( export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
@@ -21,31 +34,54 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
className, className,
containerProps, containerProps,
errorMessage, errorMessage,
id,
label, label,
labelProps, labelProps,
onChange, onChange,
onClearInput,
onValueChange, onValueChange,
placeholder, placeholder,
search,
type, type,
...restProps ...restProps
}, },
ref forwardedRef
) => { ) => {
const [showPassword, setShowPassword] = useState(false) const generatedId = useId()
const finalId = id ?? generatedId
const internalRef = useRef<HTMLInputElement>(null)
const finalRef = mergeRefs([forwardedRef, internalRef])
const [revealPassword, setRevealPassword] = useState(false)
const isShowPasswordButtonShown = type === 'password' const isRevealPasswordButtonShown = type === 'password'
const isSearchField = type === 'search'
const isClearInputButtonShown = isSearchField
const finalType = getFinalType(type, showPassword) const finalType = getFinalType(type, revealPassword)
function handleChange(e: ChangeEvent<HTMLInputElement>) { function handleChange(e: ChangeEvent<HTMLInputElement>) {
onChange?.(e) onChange?.(e)
onValueChange?.(e.target.value) onValueChange?.(e.target.value)
} }
function handleToggleShowPassword() {
setRevealPassword((prevState: boolean) => !prevState)
}
function handleClearInput() {
if (onClearInput) {
return onClearInput()
}
if (!internalRef.current) {
return
}
internalRef.current.value = ''
onValueChange?.('')
}
const classNames = { const classNames = {
error: clsx(s.error), error: clsx(s.error),
field: clsx(s.field, !!errorMessage && s.error, search && s.hasLeadingIcon, className), field: clsx(s.field, !!errorMessage && s.error, isSearchField && s.hasLeadingIcon, className),
fieldContainer: clsx(s.fieldContainer), fieldContainer: clsx(s.fieldContainer),
label: clsx(s.label, labelProps?.className), label: clsx(s.label, labelProps?.className),
leadingIcon: s.leadingIcon, leadingIcon: s.leadingIcon,
@@ -53,29 +89,42 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
} }
return ( return (
<div className={classNames.root}> <div {...containerProps} className={classNames.root}>
{label && ( {label && (
<Typography as={'label'} className={classNames.label} variant={'body2'}> <Typography
{...labelProps}
as={'label'}
className={classNames.label}
htmlFor={finalId}
variant={'body2'}
>
{label} {label}
</Typography> </Typography>
)} )}
<div className={classNames.fieldContainer}> <div className={classNames.fieldContainer}>
{search && <Search className={classNames.leadingIcon} />} {isSearchField && (
<Search
className={classNames.leadingIcon}
onClick={() => internalRef.current?.focus()}
/>
)}
<input <input
className={classNames.field} className={classNames.field}
id={finalId}
onChange={handleChange} onChange={handleChange}
placeholder={placeholder} placeholder={placeholder}
ref={ref} ref={finalRef}
type={finalType} type={finalType}
{...restProps} {...restProps}
/> />
{isShowPasswordButtonShown && ( {isRevealPasswordButtonShown && (
<button <button className={s.showPassword} onClick={handleToggleShowPassword} type={'button'}>
className={s.showPassword} {revealPassword ? <VisibilityOff /> : <Eye />}
onClick={() => setShowPassword(prev => !prev)} </button>
type={'button'} )}
> {isClearInputButtonShown && (
{showPassword ? <VisibilityOff /> : <Eye />} <button className={s.clearInput} onClick={handleClearInput} type={'button'}>
<Close height={16} width={16} />
</button> </button>
)} )}
</div> </div>
@@ -88,7 +137,10 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
} }
) )
function getFinalType(type: ComponentProps<'input'>['type'], showPassword: boolean) { function getFinalType(
type: ComponentProps<'input'>['type'],
showPassword: boolean
): ComponentProps<'input'>['type'] {
if (type === 'password' && showPassword) { if (type === 'password' && showPassword) {
return 'text' return 'text'
} }

View File

@@ -19,7 +19,7 @@ export const DeckPage = () => {
<Button as={Link} to={learnLink}> <Button as={Link} to={learnLink}>
Learn Learn
</Button> </Button>
<TextField placeholder={'Search cards'} search /> <TextField placeholder={'Search cards'} type={'search'} />
<CardsTable cards={cardsData?.items} /> <CardsTable cards={cardsData?.items} />
<Pagination <Pagination
count={cardsData?.pagination?.totalPages || 1} count={cardsData?.pagination?.totalPages || 1}

View File

@@ -134,7 +134,7 @@ export const DecksPage = () => {
<TextField <TextField
onValueChange={handleSearch} onValueChange={handleSearch}
placeholder={'Search'} placeholder={'Search'}
search type={'search'}
value={search ?? ''} value={search ?? ''}
/> />
<Tabs asChild onValueChange={handleTabChange} value={currentTab ?? undefined}> <Tabs asChild onValueChange={handleTabChange} value={currentTab ?? undefined}>

View File

@@ -1,2 +1,3 @@
export * from './date' export * from './date'
export * from './get-valuable' export * from './get-valuable'
export * from './merge-refs'

17
src/utils/merge-refs.ts Normal file
View File

@@ -0,0 +1,17 @@
//https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx
import { LegacyRef, MutableRefObject, RefCallback } from 'react'
export function mergeRefs<T = any>(
refs: Array<LegacyRef<T> | MutableRefObject<T> | null | undefined>
): RefCallback<T> {
return value => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
;(ref as MutableRefObject<T | null>).current = value
}
})
}
}