React Native accessibility (#539)
* React Native accessibility * First round of changes * Latest update * Checkpoint * Wrap up * Lint * Remove unhelpful image hints * Fix navigation * Fix rebase and lint * Mitigate an known issue with the password entry in login * Fix composer dismiss * Remove focus on input elements for web * Remove i and npm * pls work * Remove stray declaration * Regenerate yarn.lock --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
c75c888de2
commit
83959c595d
86 changed files with 2479 additions and 1827 deletions
|
@ -28,7 +28,10 @@ export const SplashScreen = ({
|
|||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}>
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Create new account"
|
||||
accessibilityHint="Opens flow to create a new Bluesky account">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
|
@ -36,7 +39,10 @@ export const SplashScreen = ({
|
|||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}>
|
||||
onPress={onPressSignin}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Sign in"
|
||||
accessibilityHint="Opens flow to sign into your existing Bluesky account">
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
@ -43,7 +43,9 @@ export const SplashScreen = ({
|
|||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}>
|
||||
onPress={onPressCreateAccount}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
|
@ -51,7 +53,9 @@ export const SplashScreen = ({
|
|||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}>
|
||||
onPress={onPressSignin}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
@ -60,7 +64,10 @@ export const SplashScreen = ({
|
|||
style={[styles.notice, pal.textLight]}
|
||||
lineHeight={1.3}>
|
||||
Bluesky will launch soon.{' '}
|
||||
<TouchableOpacity onPress={onPressWaitlist}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressWaitlist}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text type="xl" style={pal.link}>
|
||||
Join the waitlist
|
||||
</Text>
|
||||
|
|
|
@ -72,14 +72,24 @@ export const CreateAccount = observer(
|
|||
{model.step === 3 && <Step3 model={model} />}
|
||||
</View>
|
||||
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBackInner} testID="backBtn">
|
||||
<TouchableOpacity
|
||||
onPress={onPressBackInner}
|
||||
testID="backBtn"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go back"
|
||||
accessibilityHint="Navigates to the previous screen">
|
||||
<Text type="xl" style={pal.link}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{model.canNext ? (
|
||||
<TouchableOpacity testID="nextBtn" onPress={onPressNext}>
|
||||
<TouchableOpacity
|
||||
testID="nextBtn"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
{model.isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
|
@ -91,7 +101,11 @@ export const CreateAccount = observer(
|
|||
) : model.didServiceDescriptionFetchFail ? (
|
||||
<TouchableOpacity
|
||||
testID="retryConnectBtn"
|
||||
onPress={onPressRetryConnect}>
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityHint="Retries account creation"
|
||||
accessibilityLiveRegion="polite">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
</Text>
|
||||
|
|
|
@ -57,7 +57,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
<View>
|
||||
<StepHeader step="1" title="Your hosting provider" />
|
||||
<Text style={[pal.text, s.mb10]}>
|
||||
This is the company that keeps you online.
|
||||
This is the service that keeps you online.
|
||||
</Text>
|
||||
<Option
|
||||
testID="blueskyServerBtn"
|
||||
|
@ -72,7 +72,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
label="Other"
|
||||
onPress={onPressOther}>
|
||||
<View style={styles.otherForm}>
|
||||
<Text style={[pal.text, s.mb5]}>
|
||||
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
|
||||
Enter the address of your provider:
|
||||
</Text>
|
||||
<TextInput
|
||||
|
@ -82,6 +82,9 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
value={model.serviceUrl}
|
||||
editable
|
||||
onChange={onChangeServiceUrl}
|
||||
accessibilityHint="Input hosting provider address"
|
||||
accessibilityLabel="Hosting provider address"
|
||||
accessibilityLabelledBy="addressProvider"
|
||||
/>
|
||||
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||
<View style={[s.flexRow, s.mt10]}>
|
||||
|
@ -136,7 +139,12 @@ function Option({
|
|||
|
||||
return (
|
||||
<View style={[styles.option, pal.border]}>
|
||||
<TouchableWithoutFeedback onPress={onPress} testID={testID}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={`Sets hosting provider to ${label}`}>
|
||||
<View style={styles.optionHeading}>
|
||||
<View style={[styles.circle, pal.border]}>
|
||||
{isSelected ? (
|
||||
|
|
|
@ -41,6 +41,9 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
value={model.inviteCode}
|
||||
editable
|
||||
onChange={model.setInviteCode}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Invite code"
|
||||
accessibilityHint="Input invite code to proceed"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
@ -48,7 +51,11 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
{!model.inviteCode && model.isInviteCodeRequired ? (
|
||||
<Text style={[s.alignBaseline, pal.text]}>
|
||||
Don't have an invite code?{' '}
|
||||
<TouchableWithoutFeedback onPress={onPressWaitlist}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressWaitlist}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Waitlist"
|
||||
accessibilityHint="Opens Bluesky waitlist form">
|
||||
<Text style={pal.link}>Join the waitlist</Text>
|
||||
</TouchableWithoutFeedback>{' '}
|
||||
to try the beta before it's publicly available.
|
||||
|
@ -56,7 +63,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
) : (
|
||||
<>
|
||||
<View style={s.pb20}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email">
|
||||
Email address
|
||||
</Text>
|
||||
<TextInput
|
||||
|
@ -66,11 +73,17 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
value={model.email}
|
||||
editable
|
||||
onChange={model.setEmail}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityHint="Input email for Bluesky waitlist"
|
||||
accessibilityLabelledBy="email"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={s.pb20}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="password">
|
||||
Password
|
||||
</Text>
|
||||
<TextInput
|
||||
|
@ -81,17 +94,27 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
editable
|
||||
secureTextEntry
|
||||
onChange={model.setPassword}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityHint="Set password"
|
||||
accessibilityLabelledBy="password"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={s.pb20}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="legalCheck">
|
||||
Legal check
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
testID="is13Input"
|
||||
style={[styles.toggleBtn, pal.border]}
|
||||
onPress={() => model.setIs13(!model.is13)}>
|
||||
onPress={() => model.setIs13(!model.is13)}
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityLabel="Verify age"
|
||||
accessibilityHint="Verifies that I am at least 13 years of age"
|
||||
accessibilityLabelledBy="legalCheck">
|
||||
<View style={[pal.borderDark, styles.checkbox]}>
|
||||
{model.is13 && (
|
||||
<FontAwesomeIcon icon="check" style={s.blue3} size={16} />
|
||||
|
|
|
@ -23,6 +23,9 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
|
|||
value={model.handle}
|
||||
editable
|
||||
onChange={model.setHandle}
|
||||
// TODO: Add explicit text label
|
||||
accessibilityLabel="User handle"
|
||||
accessibilityHint="Input your user handle"
|
||||
/>
|
||||
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||
Your full handle will be{' '}
|
||||
|
|
|
@ -195,7 +195,10 @@ const ChooseAccountForm = ({
|
|||
testID={`chooseAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[pal.view, pal.border, styles.account]}
|
||||
onPress={() => onTryAccount(account)}>
|
||||
onPress={() => onTryAccount(account)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Sign in as ${account.handle}`}
|
||||
accessibilityHint="Double tap to sign in">
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<View style={s.p10}>
|
||||
|
@ -220,7 +223,10 @@ const ChooseAccountForm = ({
|
|||
<TouchableOpacity
|
||||
testID="chooseNewAccountBtn"
|
||||
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||
onPress={() => onSelectAccount(undefined)}>
|
||||
onPress={() => onSelectAccount(undefined)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Login to account that is not listed"
|
||||
accessibilityHint="">
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
|
@ -235,7 +241,11 @@ const ChooseAccountForm = ({
|
|||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go back"
|
||||
accessibilityHint="Navigates to the previous screen">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
|
@ -351,7 +361,10 @@ const LoginForm = ({
|
|||
<TouchableOpacity
|
||||
testID="loginSelectServiceButton"
|
||||
style={styles.textBtn}
|
||||
onPress={onPressSelectService}>
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select service"
|
||||
accessibilityHint="Sets server for the Bluesky client">
|
||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
|
@ -386,6 +399,8 @@ const LoginForm = ({
|
|||
value={identifier}
|
||||
onChangeText={str => setIdentifier((str || '').toLowerCase())}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Username or email address"
|
||||
accessibilityHint="Input the username or email address you used at signup"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
|
@ -402,14 +417,28 @@ const LoginForm = ({
|
|||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry
|
||||
// HACK
|
||||
// mitigates a known issue where the secure password prompt interferes
|
||||
// https://github.com/facebook/react-native/issues/21911
|
||||
// prf
|
||||
textContentType="oneTimeCode"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityHint={
|
||||
identifier === ''
|
||||
? 'Input your password'
|
||||
: `Input the password tied to ${identifier}`
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordButton"
|
||||
style={styles.textInputInnerBtn}
|
||||
onPress={onPressForgotPassword}>
|
||||
onPress={onPressForgotPassword}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Forgot password"
|
||||
accessibilityHint="Opens password reset form">
|
||||
<Text style={pal.link}>Forgot</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
@ -425,7 +454,11 @@ const LoginForm = ({
|
|||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go back"
|
||||
accessibilityHint="Navigates to the previous screen">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
|
@ -434,7 +467,10 @@ const LoginForm = ({
|
|||
{!serviceDescription && error ? (
|
||||
<TouchableOpacity
|
||||
testID="loginRetryButton"
|
||||
onPress={onPressRetryConnect}>
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityHint="Retries login">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
</Text>
|
||||
|
@ -449,7 +485,12 @@ const LoginForm = ({
|
|||
) : isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : isReady ? (
|
||||
<TouchableOpacity testID="loginNextButton" onPress={onPressNext}>
|
||||
<TouchableOpacity
|
||||
testID="loginNextButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
|
@ -539,7 +580,10 @@ const ForgotPasswordForm = ({
|
|||
<TouchableOpacity
|
||||
testID="forgotPasswordSelectServiceButton"
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||
onPress={onPressSelectService}>
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Hosting provider"
|
||||
accessibilityHint="Sets hosting provider for password reset">
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
|
@ -572,6 +616,8 @@ const ForgotPasswordForm = ({
|
|||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityHint="Sets email for password reset"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -586,7 +632,11 @@ const ForgotPasswordForm = ({
|
|||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go back"
|
||||
accessibilityHint="Navigates to the previous screen">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
|
@ -599,7 +649,12 @@ const ForgotPasswordForm = ({
|
|||
Next
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity testID="newPasswordButton" onPress={onPressNext}>
|
||||
<TouchableOpacity
|
||||
testID="newPasswordButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
|
@ -699,6 +754,9 @@ const SetNewPasswordForm = ({
|
|||
value={resetCode}
|
||||
onChangeText={setResetCode}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel="Reset code"
|
||||
accessibilityHint="Input code sent to your email for password reset"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
|
@ -718,6 +776,9 @@ const SetNewPasswordForm = ({
|
|||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityHint="Input new password"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -732,7 +793,11 @@ const SetNewPasswordForm = ({
|
|||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go back"
|
||||
accessibilityHint="Navigates to the previous screen">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
|
@ -747,7 +812,10 @@ const SetNewPasswordForm = ({
|
|||
) : (
|
||||
<TouchableOpacity
|
||||
testID="setNewPasswordButton"
|
||||
onPress={onPressNext}>
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
|
@ -783,7 +851,11 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
|||
</Text>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressNext}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Close alert"
|
||||
accessibilityHint="Closes password update alert">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Okay
|
||||
</Text>
|
||||
|
|
|
@ -1,27 +1,17 @@
|
|||
import React from 'react'
|
||||
import React, {ComponentProps} from 'react'
|
||||
import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
|
||||
export function TextInput({
|
||||
testID,
|
||||
icon,
|
||||
value,
|
||||
placeholder,
|
||||
editable,
|
||||
secureTextEntry,
|
||||
onChange,
|
||||
}: {
|
||||
interface Props extends Omit<ComponentProps<typeof RNTextInput>, 'onChange'> {
|
||||
testID?: string
|
||||
icon: IconProp
|
||||
value: string
|
||||
placeholder: string
|
||||
editable: boolean
|
||||
secureTextEntry?: boolean
|
||||
onChange: (v: string) => void
|
||||
}) {
|
||||
}
|
||||
|
||||
export function TextInput({testID, icon, onChange, ...props}: Props) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
|
@ -30,15 +20,12 @@ export function TextInput({
|
|||
<RNTextInput
|
||||
testID={testID}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry={secureTextEntry}
|
||||
value={value}
|
||||
onChangeText={v => onChange(v)}
|
||||
editable={editable}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue