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 = {
extends: '@it-incubator/stylelint-config',
rules: {
'value-keyword-case': null,
}
}

12097
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -89,9 +89,17 @@
background: transparent;
border: 0;
border-radius: 0.25rem;
outline: 0;
&:focus-visible {
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 { mergeRefs } from '@/utils'
import { clsx } from 'clsx'
import s from './text-field.module.scss'
@@ -11,8 +20,12 @@ export type TextFieldProps = {
errorMessage?: string
label?: string
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
search?: boolean
} & ComponentPropsWithoutRef<'input'>
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
@@ -21,31 +34,54 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
className,
containerProps,
errorMessage,
id,
label,
labelProps,
onChange,
onClearInput,
onValueChange,
placeholder,
search,
type,
...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>) {
onChange?.(e)
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 = {
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),
label: clsx(s.label, labelProps?.className),
leadingIcon: s.leadingIcon,
@@ -53,29 +89,42 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
}
return (
<div className={classNames.root}>
<div {...containerProps} className={classNames.root}>
{label && (
<Typography as={'label'} className={classNames.label} variant={'body2'}>
<Typography
{...labelProps}
as={'label'}
className={classNames.label}
htmlFor={finalId}
variant={'body2'}
>
{label}
</Typography>
)}
<div className={classNames.fieldContainer}>
{search && <Search className={classNames.leadingIcon} />}
{isSearchField && (
<Search
className={classNames.leadingIcon}
onClick={() => internalRef.current?.focus()}
/>
)}
<input
className={classNames.field}
id={finalId}
onChange={handleChange}
placeholder={placeholder}
ref={ref}
ref={finalRef}
type={finalType}
{...restProps}
/>
{isShowPasswordButtonShown && (
<button
className={s.showPassword}
onClick={() => setShowPassword(prev => !prev)}
type={'button'}
>
{showPassword ? <VisibilityOff /> : <Eye />}
{isRevealPasswordButtonShown && (
<button className={s.showPassword} onClick={handleToggleShowPassword} type={'button'}>
{revealPassword ? <VisibilityOff /> : <Eye />}
</button>
)}
{isClearInputButtonShown && (
<button className={s.clearInput} onClick={handleClearInput} type={'button'}>
<Close height={16} width={16} />
</button>
)}
</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) {
return 'text'
}

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
export * from './date'
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
}
})
}
}