mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 12:33:18 +00:00
handle search type for text field
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
module.exports = {
|
||||
extends: '@it-incubator/stylelint-config',
|
||||
rules: {
|
||||
'value-keyword-case': null,
|
||||
}
|
||||
}
|
||||
12347
pnpm-lock.yaml
generated
12347
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -134,7 +134,7 @@ export const DecksPage = () => {
|
||||
<TextField
|
||||
onValueChange={handleSearch}
|
||||
placeholder={'Search'}
|
||||
search
|
||||
type={'search'}
|
||||
value={search ?? ''}
|
||||
/>
|
||||
<Tabs asChild onValueChange={handleTabChange} value={currentTab ?? undefined}>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './date'
|
||||
export * from './get-valuable'
|
||||
export * from './merge-refs'
|
||||
|
||||
17
src/utils/merge-refs.ts
Normal file
17
src/utils/merge-refs.ts
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user