73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
import Clear from '@mui/icons-material/Clear'
|
|
import Visibility from '@mui/icons-material/Visibility'
|
|
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
|
import { IconButton, InputAdornment, TextField } from '@mui/material'
|
|
import { useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
|
|
import { verbatim } from './fonts.ts'
|
|
|
|
/** Keeps the focus in the input when one of its buttons is pressed. */
|
|
function keepFocus(event: MouseEvent<HTMLButtonElement>) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
/**
|
|
* A text field with a clear button, a show/hide button for secrets, and several lines for 'lines'. The browser
|
|
* may not change what is typed (no spell check, capitalisation or completion). A small field shows no label; the
|
|
* label remains its accessible name.
|
|
*/
|
|
export default function TextInput(props: {
|
|
label: string
|
|
value: string
|
|
kind?: 'text' | 'secret' | 'lines'
|
|
problem?: string
|
|
help?: string
|
|
disabled?: boolean
|
|
autoFocus?: boolean
|
|
start?: ReactNode
|
|
size?: 'small' | 'medium'
|
|
onChange: (value: string) => void
|
|
onBlur?: () => void
|
|
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void
|
|
}) {
|
|
const { label, value, kind = 'text', problem, help, disabled = false, onChange } = props
|
|
const [shown, setShown] = useState(false)
|
|
return (
|
|
<TextField
|
|
label={props.size === 'small' ? undefined : label}
|
|
value={value}
|
|
size={props.size ?? (kind === 'lines' ? 'medium' : undefined)}
|
|
fullWidth
|
|
onBlur={props.onBlur}
|
|
onKeyDown={props.onKeyDown}
|
|
autoFocus={props.autoFocus}
|
|
disabled={disabled}
|
|
error={problem !== undefined}
|
|
helperText={props.size === 'small' ? problem : problem || help || ' '}
|
|
multiline={kind === 'lines'}
|
|
minRows={kind === 'lines' ? 2 : undefined}
|
|
type={kind === 'secret' && !shown ? 'password' : 'text'}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
slotProps={{
|
|
htmlInput: { ...verbatim, 'aria-label': label },
|
|
input: {
|
|
startAdornment: props.start === undefined ? undefined : <InputAdornment position="start">{props.start}</InputAdornment>,
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
{kind === 'secret' && (
|
|
<IconButton aria-label={shown ? `hide ${label}` : `show ${label}`} disabled={disabled} onClick={() => setShown(!shown)} onMouseDown={keepFocus} onMouseUp={keepFocus}>
|
|
{shown ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
)}
|
|
{value !== '' && (
|
|
<IconButton aria-label={`clear ${label}`} disabled={disabled} edge="end" onClick={() => onChange('')} onMouseDown={keepFocus} onMouseUp={keepFocus}>
|
|
<Clear />
|
|
</IconButton>
|
|
)}
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
/>
|
|
)
|
|
}
|