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
This commit is contained in:
@@ -288,8 +288,11 @@ existing usages across the app for a canonical example.
|
||||
### TextField
|
||||
|
||||
Compound component at `#/components/forms/TextField` (`TextField.LabelText`,
|
||||
`TextField.Root`, `TextField.Icon`, `TextField.Input`). Prefer `defaultValue` over
|
||||
`value` (see Footguns).
|
||||
`TextField.Root`, `TextField.Icon`, `TextField.Input`). Controlled inputs
|
||||
(`value` + `onChangeText`) are fine and are usually what you want - the old
|
||||
advice to reach for `defaultValue` was a New Architecture migration concern and
|
||||
no longer applies. Reach for `defaultValue` only when nothing outside the input
|
||||
needs to read the text.
|
||||
|
||||
### Typography
|
||||
|
||||
@@ -507,24 +510,6 @@ This applies to:
|
||||
|
||||
The Menu component on iOS specifically uses this pattern – see `src/components/Menu/index.tsx:151`.
|
||||
|
||||
### Controlled vs Uncontrolled Inputs
|
||||
|
||||
Prefer `defaultValue` over `value` for TextInput on the old architecture:
|
||||
|
||||
```tsx
|
||||
// Preferred - uncontrolled
|
||||
<TextField.Input
|
||||
defaultValue={initialEmail}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
|
||||
// Avoid when possible - controlled (can cause performance issues)
|
||||
<TextField.Input
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
```
|
||||
|
||||
### Platform-Specific Behavior
|
||||
|
||||
Some components behave differently across platforms:
|
||||
|
||||
@@ -139,7 +139,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const [searchText, setSearchText] = useState(lastSearchText)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
@@ -305,7 +304,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const onSelectTab = useCallback(
|
||||
(interest: string) => {
|
||||
setSelectedInterest(interest)
|
||||
inputRef.current?.clear()
|
||||
setSearchText('')
|
||||
listRef.current?.scrollToOffset({
|
||||
offset: 0,
|
||||
@@ -318,7 +316,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const listHeader = (
|
||||
<Header
|
||||
guide={guide}
|
||||
inputRef={inputRef}
|
||||
listRef={listRef}
|
||||
searchText={searchText}
|
||||
onSelectTab={onSelectTab}
|
||||
@@ -357,7 +354,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
|
||||
let Header = ({
|
||||
guide,
|
||||
inputRef,
|
||||
listRef,
|
||||
searchText,
|
||||
onSelectTab,
|
||||
@@ -368,7 +364,6 @@ let Header = ({
|
||||
interestsDisplayNames,
|
||||
}: {
|
||||
guide?: Follow10ProgressGuide
|
||||
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||
listRef: React.RefObject<ListMethods | null>
|
||||
onSelectTab: (v: string) => void
|
||||
searchText: string
|
||||
@@ -396,8 +391,7 @@ let Header = ({
|
||||
|
||||
<View style={[web(a.pt_xs), a.pb_xs]}>
|
||||
<SearchInput
|
||||
inputRef={inputRef}
|
||||
defaultValue={searchText}
|
||||
value={searchText}
|
||||
onChangeText={text => {
|
||||
setSearchText(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
@@ -668,13 +662,11 @@ function CardOuter({
|
||||
function SearchInput({
|
||||
onChangeText,
|
||||
onEscape,
|
||||
inputRef,
|
||||
defaultValue,
|
||||
value,
|
||||
}: {
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||
defaultValue: string
|
||||
value: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
@@ -698,9 +690,8 @@ function SearchInput({
|
||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||
/>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder={l`Search by name or interest`}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
|
||||
@@ -58,7 +58,12 @@ export function OTPInput({
|
||||
style={[a.w_full, a.relative]}
|
||||
onPress={() => {
|
||||
innerRef.current?.focus()
|
||||
innerRef.current?.clear()
|
||||
/*
|
||||
* Clear through state, not the input's imperative `clear()`: the digit
|
||||
* row above renders from `value`, so a native-only clear would leave
|
||||
* stale digits on screen.
|
||||
*/
|
||||
onChange('')
|
||||
}}>
|
||||
<View style={[a.w_full, a.flex_row, a.gap_sm]}>
|
||||
{[...value.padEnd(numberOfDigits, ' ')].map((digit, index) => {
|
||||
|
||||
@@ -218,12 +218,9 @@ export function Update(_props: ScreenProps<ScreenID.Update>) {
|
||||
<TextField.Input
|
||||
label={l`New email address`}
|
||||
placeholder={l`alice@example.com`}
|
||||
defaultValue={state.email}
|
||||
onChangeText={
|
||||
state.mutationStatus === 'success'
|
||||
? undefined
|
||||
: handleEmailChange
|
||||
}
|
||||
value={state.email}
|
||||
editable={state.mutationStatus !== 'success'}
|
||||
onChangeText={handleEmailChange}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -421,7 +421,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={displayNameTooLong || displayNameTooShort}>
|
||||
<Dialog.Input
|
||||
defaultValue={displayName}
|
||||
value={displayName}
|
||||
onChangeText={onChangeDisplayName}
|
||||
label={_(msg`Name`)}
|
||||
placeholder={displayNamePlaceholder}
|
||||
@@ -457,7 +457,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={descriptionTooLong}>
|
||||
<Dialog.Input
|
||||
defaultValue={descriptionRt.text}
|
||||
value={descriptionRt.text}
|
||||
onChangeText={onChangeDescription}
|
||||
multiline
|
||||
label={_(msg`Description`)}
|
||||
|
||||
@@ -156,13 +156,6 @@ export type InputProps = Omit<
|
||||
'value' | 'onChangeText' | 'placeholder'
|
||||
> & {
|
||||
label: string
|
||||
/**
|
||||
* @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible.
|
||||
*
|
||||
* See https://github.com/facebook/react-native-website/pull/4247
|
||||
*
|
||||
* Note: This guidance no longer applies once we migrate to the New Architecture!
|
||||
*/
|
||||
value?: string
|
||||
onChangeText?: (value: string) => void
|
||||
isInvalid?: boolean
|
||||
|
||||
@@ -115,7 +115,6 @@ function GifPickerBody({
|
||||
}, [effectiveSearch, isRecentsActive])
|
||||
|
||||
const onClearSearch = () => {
|
||||
textInputRef.current?.clear()
|
||||
setRawSearch('')
|
||||
setActiveCategory('trending')
|
||||
textInputRef.current?.focus()
|
||||
@@ -148,9 +147,9 @@ function GifPickerBody({
|
||||
<>
|
||||
<GifPickerHeader
|
||||
inputRef={textInputRef}
|
||||
value={rawSearch}
|
||||
onChangeText={onChangeSearch}
|
||||
onClear={onClearSearch}
|
||||
canClear={rawSearch.length > 0}
|
||||
onEscape={() => control.close()}
|
||||
/>
|
||||
{showPills && (
|
||||
|
||||
@@ -10,16 +10,16 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
|
||||
export function GifPickerHeader({
|
||||
inputRef,
|
||||
value,
|
||||
onChangeText,
|
||||
onClear,
|
||||
onEscape,
|
||||
canClear,
|
||||
}: {
|
||||
inputRef: Ref<React.ComponentRef<typeof TextInput>>
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
onClear: () => void
|
||||
onEscape: () => void
|
||||
canClear: boolean
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -47,6 +47,7 @@ export function GifPickerHeader({
|
||||
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}
|
||||
@@ -57,7 +58,7 @@ export function GifPickerHeader({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{canClear && (
|
||||
{value.length > 0 && (
|
||||
<Button
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
|
||||
@@ -73,15 +73,13 @@ export const LoginForm = ({
|
||||
>('none')
|
||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false)
|
||||
const [showResolveError, setShowResolveError] = useState(false)
|
||||
const identifierValueRef = useRef(initialHandle || '')
|
||||
const passwordValueRef = useRef('')
|
||||
const [identifier, setIdentifier] = useState(initialHandle || '')
|
||||
const [password, setPassword] = useState('')
|
||||
const [identifierFocused, setIdentifierFocused] = useState(false)
|
||||
const [authFactorToken, setAuthFactorToken] = useState('')
|
||||
const identifierRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const passwordRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const hasFocusedOnce = useRef(false)
|
||||
const [hasPassword, setHasPassword] = useState(false)
|
||||
const [revealPassword, setRevealPassword] = useState(false)
|
||||
const {t: l} = useLingui()
|
||||
const {login} = useSessionApi()
|
||||
@@ -119,7 +117,6 @@ export const LoginForm = ({
|
||||
* transitions away) and clears it on any failure.
|
||||
*/
|
||||
const attemptLogin = async (service: string, fullIdent: string) => {
|
||||
const password = passwordValueRef.current
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
@@ -184,10 +181,9 @@ export const LoginForm = ({
|
||||
setErrorField('none')
|
||||
setShowResolveError(false)
|
||||
|
||||
const identifier = identifierValueRef.current.toLowerCase().trim()
|
||||
const password = passwordValueRef.current
|
||||
const normalizedIdentifier = identifier.toLowerCase().trim()
|
||||
|
||||
if (!identifier) {
|
||||
if (!normalizedIdentifier) {
|
||||
setError(l`Please enter your username`)
|
||||
setErrorField('identifier')
|
||||
return
|
||||
@@ -202,11 +198,11 @@ export const LoginForm = ({
|
||||
setIsProcessing(true)
|
||||
|
||||
// try to guess the handle if the user just gave their own username
|
||||
let fullIdent = identifier
|
||||
let fullIdent = normalizedIdentifier
|
||||
if (
|
||||
!identifier.includes('@') && // not an email
|
||||
!identifier.includes('.') && // not a domain
|
||||
!identifier.startsWith('did:') && // not a DID
|
||||
!normalizedIdentifier.includes('@') && // not an email
|
||||
!normalizedIdentifier.includes('.') && // not a domain
|
||||
!normalizedIdentifier.startsWith('did:') && // not a DID
|
||||
serviceDescription &&
|
||||
serviceDescription.availableUserDomains.length > 0
|
||||
) {
|
||||
@@ -218,7 +214,7 @@ export const LoginForm = ({
|
||||
}
|
||||
if (!matched) {
|
||||
fullIdent = createFullHandle(
|
||||
identifier,
|
||||
normalizedIdentifier,
|
||||
serviceDescription.availableUserDomains[0],
|
||||
)
|
||||
}
|
||||
@@ -235,7 +231,8 @@ export const LoginForm = ({
|
||||
let service: string
|
||||
let did: string | null
|
||||
try {
|
||||
;({service, did} = await hostingProvider.resolveService(identifier))
|
||||
;({service, did} =
|
||||
await hostingProvider.resolveService(normalizedIdentifier))
|
||||
} catch (err) {
|
||||
logger.debug('Failed to resolve hosting provider', {error: String(err)})
|
||||
setIsProcessing(false)
|
||||
@@ -321,9 +318,8 @@ export const LoginForm = ({
|
||||
autoComplete="username"
|
||||
returnKeyType="next"
|
||||
textContentType="username"
|
||||
defaultValue={initialHandle || ''}
|
||||
value={identifier}
|
||||
onChangeText={v => {
|
||||
identifierValueRef.current = v
|
||||
setIdentifier(v)
|
||||
if (errorField) setErrorField('none')
|
||||
if (showResolveError) setShowResolveError(false)
|
||||
@@ -379,10 +375,10 @@ export const LoginForm = ({
|
||||
returnKeyType="done"
|
||||
enablesReturnKeyAutomatically={true}
|
||||
secureTextEntry={!revealPassword}
|
||||
value={password}
|
||||
onChangeText={v => {
|
||||
passwordValueRef.current = v
|
||||
setPassword(v)
|
||||
if (errorField) setErrorField('none')
|
||||
setHasPassword(!!v)
|
||||
}}
|
||||
onSubmitEditing={() => void onPressNext()}
|
||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||
@@ -412,7 +408,7 @@ export const LoginForm = ({
|
||||
/>
|
||||
<RevealPasswordButton
|
||||
active={revealPassword}
|
||||
hasPassword={hasPassword}
|
||||
hasPassword={!!password}
|
||||
onPress={() => setRevealPassword(r => !r)}
|
||||
/>
|
||||
</TextField.Root>
|
||||
@@ -447,7 +443,7 @@ export const LoginForm = ({
|
||||
autoComplete="one-time-code"
|
||||
returnKeyType="done"
|
||||
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||
value={authFactorToken} // controlled input due to uncontrolled input not receiving pasted values properly
|
||||
value={authFactorToken}
|
||||
onChangeText={text => {
|
||||
setAuthFactorToken(text)
|
||||
if (errorField) setErrorField('none')
|
||||
|
||||
@@ -339,11 +339,9 @@ function SettingsHeader({
|
||||
|
||||
const groupName = convo.details.name
|
||||
const [newGroupName, setNewGroupName] = useState(groupName)
|
||||
const [editNameInputKey, setEditNameInputKey] = useState(0)
|
||||
|
||||
const openEditNamePrompt = () => {
|
||||
setNewGroupName(groupName)
|
||||
setEditNameInputKey(k => k + 1)
|
||||
editNamePrompt.open()
|
||||
}
|
||||
|
||||
@@ -591,7 +589,6 @@ function SettingsHeader({
|
||||
<EditNamePrompt
|
||||
control={editNamePrompt}
|
||||
value={newGroupName}
|
||||
inputKey={editNameInputKey}
|
||||
onChangeText={setNewGroupName}
|
||||
onConfirm={() => editGroupName({name: newGroupName})}
|
||||
/>
|
||||
|
||||
@@ -12,18 +12,11 @@ import {Text} from '#/components/Typography'
|
||||
export function EditNamePrompt({
|
||||
control,
|
||||
value,
|
||||
inputKey,
|
||||
onChangeText,
|
||||
onConfirm,
|
||||
}: {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
value: string
|
||||
/**
|
||||
* Bump this whenever the prompt is opened to remount the (uncontrolled)
|
||||
* input and reseed it from `value`. Required because the native bottom sheet
|
||||
* keeps its children mounted across opens.
|
||||
*/
|
||||
inputKey: number
|
||||
onChangeText: (value: string) => void
|
||||
onConfirm: () => void
|
||||
}) {
|
||||
@@ -45,10 +38,9 @@ export function EditNamePrompt({
|
||||
<View style={[a.my_sm]}>
|
||||
<TextField.Root isInvalid={nameTooLong}>
|
||||
<TextField.Input
|
||||
key={inputKey}
|
||||
label={l`Edit group name`}
|
||||
placeholder={l`Group name`}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -311,7 +311,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={displayNameTooLong}>
|
||||
<Dialog.Input
|
||||
defaultValue={displayName}
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
label={_(msg`Display name`)}
|
||||
placeholder={_(msg`e.g. Alice Lastname`)}
|
||||
@@ -361,7 +361,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={descriptionTooLong}>
|
||||
<Dialog.Input
|
||||
defaultValue={description}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
label={_(msg`Description`)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {useRef, useState} from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -9,59 +8,47 @@ import * as TextField from '#/components/forms/TextField'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
|
||||
/**
|
||||
* A text input with a clear (X) button inside it on the right. The input stays
|
||||
* uncontrolled (defaultValue + imperative clear) per the codebase's preference;
|
||||
* local state only drives whether the clear button is shown.
|
||||
* A text input with a clear (X) button inside it on the right.
|
||||
*/
|
||||
export function ClearableInput({
|
||||
label,
|
||||
defaultValue,
|
||||
value,
|
||||
placeholder,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
}: {
|
||||
label: string
|
||||
defaultValue: string
|
||||
value: string
|
||||
placeholder?: string
|
||||
onChangeText: (text: string) => void
|
||||
onSubmitEditing?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const [showClear, setShowClear] = useState(defaultValue.length > 0)
|
||||
|
||||
return (
|
||||
<View style={[a.relative]}>
|
||||
<TextField.Root>
|
||||
<Dialog.Input
|
||||
inputRef={inputRef}
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
keyboardAppearance={t.scheme}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
style={[a.pr_2xl]}
|
||||
onChangeText={text => {
|
||||
setShowClear(text.length > 0)
|
||||
onChangeText(text)
|
||||
}}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{showClear && (
|
||||
{value.length > 0 && (
|
||||
<View
|
||||
style={[a.absolute, a.justify_center, {top: 0, bottom: 0, right: 8}]}>
|
||||
<Button
|
||||
label={l`Clear`}
|
||||
onPress={() => {
|
||||
inputRef.current?.clear()
|
||||
setShowClear(false)
|
||||
onChangeText('')
|
||||
}}
|
||||
onPress={() => onChangeText('')}
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
shape="round">
|
||||
|
||||
@@ -154,11 +154,6 @@ export function FilterBlock({
|
||||
|
||||
{HANDLE_FIELDS.has(filter.field) ? (
|
||||
<AutocompleteInput
|
||||
/*
|
||||
* Remount on field-type change so the input resets to the cleared
|
||||
* value; mode changes keep the same field and so preserve the text.
|
||||
*/
|
||||
key={filter.field}
|
||||
label={labels[filter.field].label}
|
||||
value={filter.value}
|
||||
onChangeText={text => onChange({value: text})}
|
||||
@@ -166,13 +161,8 @@ export function FilterBlock({
|
||||
/>
|
||||
) : (
|
||||
<ClearableInput
|
||||
/*
|
||||
* The input is uncontrolled (defaultValue), so remount on field-type
|
||||
* change to reset it; mode changes keep the same field and text.
|
||||
*/
|
||||
key={filter.field}
|
||||
label={labels[filter.field].label}
|
||||
defaultValue={filter.value}
|
||||
value={filter.value}
|
||||
onChangeText={text => onChange({value: text})}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
/>
|
||||
|
||||
@@ -246,7 +246,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<ClearableInput
|
||||
label={l`Search query`}
|
||||
defaultValue={query}
|
||||
value={query}
|
||||
placeholder={l({
|
||||
message: 'cats dogs',
|
||||
comment:
|
||||
@@ -264,7 +264,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<ClearableInput
|
||||
label={l`None of these words`}
|
||||
defaultValue={negatedWords}
|
||||
value={negatedWords}
|
||||
placeholder={l({
|
||||
message: 'cows pigs',
|
||||
comment:
|
||||
@@ -281,7 +281,7 @@ function DialogInner({
|
||||
</TextField.LabelText>
|
||||
<ClearableInput
|
||||
label={l`This exact phrase`}
|
||||
defaultValue={exactPhrase}
|
||||
value={exactPhrase}
|
||||
placeholder={l({
|
||||
message: 'what’s up',
|
||||
comment: 'Advanced search: Example of an “exact phrase” search',
|
||||
|
||||
@@ -121,6 +121,7 @@ function CreateDialogInner({passwords}: {passwords: string[]}) {
|
||||
<Dialog.Input
|
||||
label={_(msg`App Password`)}
|
||||
placeholder={autogeneratedName}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={() => createAppPassword()}
|
||||
|
||||
@@ -231,7 +231,7 @@ function ProvidedHandlePage({
|
||||
<TextField.Icon icon={AtIcon} />
|
||||
<Dialog.Input
|
||||
editable={!isPending}
|
||||
defaultValue={subdomain}
|
||||
value={subdomain}
|
||||
onChangeText={text => setSubdomain(text)}
|
||||
label={_(msg`New handle`)}
|
||||
placeholder={_(msg`e.g. alice`)}
|
||||
@@ -387,7 +387,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
label={_(msg`New handle`)}
|
||||
placeholder={_(msg`e.g. alice.com`)}
|
||||
editable={!isPending}
|
||||
defaultValue={domain}
|
||||
value={domain}
|
||||
onChangeText={text => {
|
||||
setDomain(text)
|
||||
resetVerification()
|
||||
|
||||
@@ -76,10 +76,11 @@ export function StepInfo({
|
||||
const preemptivelyCompleteActivePolicyUpdate =
|
||||
usePreemptivelyCompleteActivePolicyUpdate()
|
||||
|
||||
const inviteCodeValueRef = useRef<string>(state.inviteCode)
|
||||
const emailValueRef = useRef<string>(state.email)
|
||||
/**
|
||||
* The last email we ran the "did you really mean this?" check against, so
|
||||
* the warning is only shown once per distinct address.
|
||||
*/
|
||||
const prevEmailValueRef = useRef<string>(state.email)
|
||||
const passwordValueRef = useRef<string>(state.password)
|
||||
|
||||
const emailInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const passwordInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
@@ -111,10 +112,8 @@ export function StepInfo({
|
||||
}, [])
|
||||
|
||||
const onNextPress = () => {
|
||||
const inviteCode = inviteCodeValueRef.current
|
||||
const email = emailValueRef.current
|
||||
const {inviteCode, email, password} = state
|
||||
const emailChanged = prevEmailValueRef.current !== email
|
||||
const password = passwordValueRef.current
|
||||
|
||||
if (!isOverRegionMinAccessAge) {
|
||||
return
|
||||
@@ -170,9 +169,6 @@ export function StepInfo({
|
||||
}
|
||||
|
||||
preemptivelyCompleteActivePolicyUpdate()
|
||||
dispatch({type: 'setInviteCode', value: inviteCode})
|
||||
dispatch({type: 'setEmail', value: email})
|
||||
dispatch({type: 'setPassword', value: password})
|
||||
dispatch({type: 'next'})
|
||||
ax.metric('signup:nextPressed', {
|
||||
activeStep: state.activeStep,
|
||||
@@ -207,7 +203,7 @@ export function StepInfo({
|
||||
<TextField.Icon icon={Ticket} />
|
||||
<TextField.Input
|
||||
onChangeText={value => {
|
||||
inviteCodeValueRef.current = value.trim()
|
||||
dispatch({type: 'setInviteCode', value: value.trim()})
|
||||
if (
|
||||
state.errorField === 'invite-code' &&
|
||||
value.trim().length > 0
|
||||
@@ -216,7 +212,7 @@ export function StepInfo({
|
||||
}
|
||||
}}
|
||||
label={l`Required for this provider`}
|
||||
defaultValue={state.inviteCode}
|
||||
value={state.inviteCode}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
@@ -239,7 +235,7 @@ export function StepInfo({
|
||||
testID="emailInput"
|
||||
inputRef={emailInputRef}
|
||||
onChangeText={value => {
|
||||
emailValueRef.current = value.trim()
|
||||
dispatch({type: 'setEmail', value: value.trim()})
|
||||
if (hasWarnedEmail) {
|
||||
setHasWarnedEmail(false)
|
||||
}
|
||||
@@ -252,7 +248,7 @@ export function StepInfo({
|
||||
}
|
||||
}}
|
||||
label={l`Enter your email address`}
|
||||
defaultValue={state.email}
|
||||
value={state.email}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
@@ -274,13 +270,13 @@ export function StepInfo({
|
||||
testID="passwordInput"
|
||||
inputRef={passwordInputRef}
|
||||
onChangeText={value => {
|
||||
passwordValueRef.current = value
|
||||
dispatch({type: 'setPassword', value})
|
||||
if (state.errorField === 'password' && value.length >= 8) {
|
||||
dispatch({type: 'clearError'})
|
||||
}
|
||||
}}
|
||||
label={l`Choose your password`}
|
||||
defaultValue={state.password}
|
||||
value={state.password}
|
||||
secureTextEntry
|
||||
autoComplete="new-password"
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -162,7 +162,7 @@ export function Takendown() {
|
||||
}>
|
||||
<TextField.Input
|
||||
label={_(msg`Reason for appeal`)}
|
||||
defaultValue={reason}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
placeholder={_(msg`Why are you appealing?`)}
|
||||
multiline
|
||||
|
||||
@@ -161,7 +161,7 @@ function AltTextInner({
|
||||
label={_(msg`Alt text`)}
|
||||
placeholder={vendorAltText}
|
||||
onChangeText={onChange}
|
||||
defaultValue={altText}
|
||||
value={altText}
|
||||
multiline
|
||||
autoFocus
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
|
||||
@@ -129,7 +129,7 @@ const ImageAltTextInner = ({
|
||||
onChangeText={text => {
|
||||
setAltText(text)
|
||||
}}
|
||||
defaultValue={altText}
|
||||
value={altText}
|
||||
multiline
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user