mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
handle search type for text field
This commit is contained in:
@@ -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
12347
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
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