Files
bsky-social-app/src/features/gifPicker/components/GifPickerHeader.tsx
T
Claude b111676744 Drop the controlled-input warning and adopt controlled inputs
`TextField.Input`'s `value` prop carried a `@deprecated` tag steering
everyone toward `defaultValue`. That was an old-architecture performance
concern; we're on the New Architecture now, so the warning is gone and
CLAUDE.md says controlled inputs are the default choice.

Audited the uncontrolled inputs that warning produced. The interesting
ones were carrying workarounds to paper over the fact that the input and
the state describing it could drift apart:

- Advanced search: `ClearableInput` kept its own `showClear` state and
  cleared itself through a ref, and `FilterBlock` remounted it (and the
  already-controlled `AutocompleteInput`) with `key={filter.field}` to
  reseed it. All three are gone.
- Group chat: `EditNamePrompt` took an `inputKey` that
  `ConversationSettings` bumped on every open to remount the input,
  because the native bottom sheet keeps children mounted across opens.
- Follow dialog and GIF picker: both cleared their search field through
  a ref alongside the state update. The GIF picker's field had no
  `value` at all, so its clear button was driven by a separate prop.
- Login and signup: both mirrored every field into a ref so submit could
  read it, keeping two copies of the same string. Signup's
  `prevEmailValueRef` stays - it tracks the last email we warned about,
  not the input.
- The email dialog froze its field by dropping `onChangeText` after a
  successful update, which stopped recording edits but didn't stop them
  being typed. Now it's `editable={false}`.

The rest were plain `defaultValue` + `setState` pairs where the state
was already the source of truth for validation, character counters and
dirty checks: profile and list editing, both alt text dialogs, appeal
reason, handle change, app password name.

Also fixed `OTPInput`, which was controlled but cleared itself through
`clear()` on tap - the digit row renders from `value`, so the code
appeared to survive a clear that had already emptied the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016imsvP5dtEKBr6hCy1X62K
2026-09-02 22:15:43 +00:00

80 lines
2.3 KiB
TypeScript

import {type Ref} from 'react'
import {type TextInput, View} from 'react-native'
import {useLingui} from '@lingui/react/macro'
import {atoms as a, native, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
export function GifPickerHeader({
inputRef,
value,
onChangeText,
onClear,
onEscape,
}: {
inputRef: Ref<React.ComponentRef<typeof TextInput>>
value: string
onChangeText: (text: string) => void
onClear: () => void
onEscape: () => void
}) {
const {t: l} = useLingui()
const t = useTheme()
return (
<View
style={[
native(a.pt_4xl),
a.relative,
a.pb_md,
a.flex_row,
a.align_center,
t.atoms.bg,
]}>
<TextField.Root style={a.flex_1}>
<TextField.Icon icon={Search} />
<TextField.Input
label={l({
message: 'Search GIFs',
comment:
'Accessibility label for the GIF search input inside the GIF picker dialog.',
})}
placeholder={l({
message: 'Search KLIPY',
comment:
'Placeholder text inside the GIF search input. KLIPY is the third-party GIF provider; keep the brand name as-is.',
})}
value={value}
onChangeText={onChangeText}
returnKeyType="search"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
}
}}
/>
{value.length > 0 && (
<Button
size="tiny"
color="secondary"
shape="round"
style={a.z_30}
onPress={onClear}
label={l({
message: 'Clear GIF search',
comment:
'Accessibility label for the X button inside the search input that clears the typed query and returns to the trending feed.',
})}>
<ButtonIcon icon={X} size="sm" />
</Button>
)}
</TextField.Root>
</View>
)
}