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:
Ollie H 2023-05-01 18:38:47 -07:00 committed by GitHub
parent c75c888de2
commit 83959c595d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2479 additions and 1827 deletions

View file

@ -122,12 +122,18 @@ export function Component({}: {}) {
editable={!appPassword}
returnKeyType="done"
onEndEditing={createAppPassword}
accessible={true}
accessibilityLabel="Name"
accessibilityHint="Input name for app password"
/>
</View>
) : (
<TouchableOpacity
style={[pal.border, styles.passwordContainer, pal.btn]}
onPress={onCopy}>
onPress={onCopy}
accessibilityRole="button"
accessibilityLabel="Copy"
accessibilityHint="Copies app password">
<Text type="2xl-bold" style={[pal.text]}>
{appPassword}
</Text>

View file

@ -37,7 +37,8 @@ export function Component({prevAltText, onAltTextSet}: Props) {
return (
<View
testID="altTextImageModal"
style={[pal.view, styles.container, s.flex1]}>
style={[pal.view, styles.container, s.flex1]}
nativeID="imageAltText">
<Text style={[styles.title, pal.text]}>Add alt text</Text>
<TextInput
testID="altTextImageInput"
@ -46,9 +47,17 @@ export function Component({prevAltText, onAltTextSet}: Props) {
multiline
value={altText}
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
accessibilityLabel="Image alt text"
accessibilityHint="Sets image alt text for screenreaders"
accessibilityLabelledBy="imageAltText"
/>
<View style={styles.buttonControls}>
<TouchableOpacity testID="altTextImageSaveBtn" onPress={onPressSave}>
<TouchableOpacity
testID="altTextImageSaveBtn"
onPress={onPressSave}
accessibilityLabel="Save alt text"
accessibilityHint={`Saves alt text, which reads: ${altText}`}
accessibilityRole="button">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
@ -61,7 +70,11 @@ export function Component({prevAltText, onAltTextSet}: Props) {
</TouchableOpacity>
<TouchableOpacity
testID="altTextImageCancelBtn"
onPress={onPressCancel}>
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel add image alt text"
accessibilityHint="Exits adding alt text to image"
onAccessibilityEscape={onPressCancel}>
<View style={[styles.button]}>
<Text type="button-lg" style={[pal.textLight]}>
Cancel

View file

@ -30,7 +30,12 @@ export function Component({altText}: Props) {
<View style={[styles.text, pal.viewLight]}>
<Text style={pal.text}>{altText}</Text>
</View>
<TouchableOpacity testID="altTextImageSaveBtn" onPress={onPress}>
<TouchableOpacity
testID="altTextImageSaveBtn"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityHint="Save alt text">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View file

@ -133,7 +133,12 @@ export function Component({onChanged}: {onChanged: () => void}) {
<View style={[s.flex1, pal.view]}>
<View style={[styles.title, pal.border]}>
<View style={styles.titleLeft}>
<TouchableOpacity onPress={onPressCancel}>
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel change handle"
accessibilityHint="Exits handle change process"
onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}>
Cancel
</Text>
@ -148,13 +153,20 @@ export function Component({onChanged}: {onChanged: () => void}) {
) : error && !serviceDescription ? (
<TouchableOpacity
testID="retryConnectButton"
onPress={onPressRetryConnect}>
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel="Retry change handle"
accessibilityHint={`Retries handle change to ${handle}`}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Retry
</Text>
</TouchableOpacity>
) : canSave ? (
<TouchableOpacity onPress={onPressSave}>
<TouchableOpacity
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel="Save handle change"
accessibilityHint={`Saves handle change to ${handle}`}>
<Text type="2xl-medium" style={pal.link}>
Save
</Text>
@ -245,6 +257,9 @@ function ProvidedHandleForm({
value={handle}
onChangeText={onChangeHandle}
editable={!isProcessing}
accessible={true}
accessibilityLabel="Handle"
accessibilityHint="Sets Bluesky username"
/>
</View>
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
@ -253,7 +268,11 @@ function ProvidedHandleForm({
@{createFullHandle(handle, userDomain)}
</Text>
</Text>
<TouchableOpacity onPress={onToggleCustom}>
<TouchableOpacity
onPress={onToggleCustom}
accessibilityRole="button"
accessibilityHint="Hosting provider"
accessibilityLabel="Opens modal for using custom domain">
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
I have my own domain
</Text>
@ -338,7 +357,7 @@ function CustomHandleForm({
// =
return (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
<Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
Enter the domain you want to use
</Text>
<View style={[pal.btn, styles.textInputWrapper]}>
@ -356,6 +375,9 @@ function CustomHandleForm({
value={handle}
onChangeText={onChangeHandle}
editable={!isProcessing}
accessibilityLabelledBy="customDomain"
accessibilityLabel="Custom domain"
accessibilityHint="Input your preferred hosting provider"
/>
</View>
<View style={styles.spacer} />
@ -421,7 +443,10 @@ function CustomHandleForm({
)}
</Button>
<View style={styles.spacer} />
<TouchableOpacity onPress={onToggleCustom}>
<TouchableOpacity
onPress={onToggleCustom}
accessibilityLabel="Use default provider"
accessibilityHint="Use bsky.social as hosting provider">
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
Nevermind, create a handle for me
</Text>

View file

@ -66,7 +66,12 @@ export function Component({
<TouchableOpacity
testID="confirmBtn"
onPress={onPress}
style={[styles.btn]}>
style={[styles.btn]}
accessibilityRole="button"
accessibilityLabel="Confirm"
// TODO: This needs to be updated so that modal roles are clear;
// Currently there is only one usage for the confirm modal: post deletion
accessibilityHint="Confirms a potentially destructive action">
<Text style={[s.white, s.bold, s.f18]}>Confirm</Text>
</TouchableOpacity>
)}

View file

@ -34,7 +34,12 @@ export function Component({}: {}) {
<View style={styles.bottomSpacer} />
</ScrollView>
<View style={[styles.btnContainer, pal.borderDark]}>
<Pressable testID="sendReportBtn" onPress={onPressDone}>
<Pressable
testID="sendReportBtn"
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel="Confirm content moderation settings"
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
@ -48,6 +53,7 @@ export function Component({}: {}) {
)
}
// TODO: Refactor this component to pass labels down to each tab
const ContentLabelPref = observer(
({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
const store = useStores()
@ -67,19 +73,20 @@ const ContentLabelPref = observer(
<SelectGroup
current={store.preferences.contentLabels[group]}
onChange={v => store.preferences.setContentLabelPref(group, v)}
group={group}
/>
</View>
)
},
)
function SelectGroup({
current,
onChange,
}: {
interface SelectGroupProps {
current: LabelPreference
onChange: (v: LabelPreference) => void
}) {
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
}
function SelectGroup({current, onChange, group}: SelectGroupProps) {
return (
<View style={styles.selectableBtns}>
<SelectableBtn
@ -88,12 +95,14 @@ function SelectGroup({
label="Hide"
left
onChange={onChange}
group={group}
/>
<SelectableBtn
current={current}
value="warn"
label="Warn"
onChange={onChange}
group={group}
/>
<SelectableBtn
current={current}
@ -101,11 +110,22 @@ function SelectGroup({
label="Show"
right
onChange={onChange}
group={group}
/>
</View>
)
}
interface SelectableBtnProps {
current: string
value: LabelPreference
label: string
left?: boolean
right?: boolean
onChange: (v: LabelPreference) => void
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
}
function SelectableBtn({
current,
value,
@ -113,14 +133,8 @@ function SelectableBtn({
left,
right,
onChange,
}: {
current: string
value: LabelPreference
label: string
left?: boolean
right?: boolean
onChange: (v: LabelPreference) => void
}) {
group,
}: SelectableBtnProps) {
const pal = usePalette('default')
const palPrimary = usePalette('inverted')
return (
@ -132,7 +146,10 @@ function SelectableBtn({
pal.border,
current === value ? palPrimary.view : pal.view,
]}
onPress={() => onChange(value)}>
onPress={() => onChange(value)}
accessibilityRole="button"
accessibilityLabel={value}
accessibilityHint={`Set ${value} for ${group} content moderation policy`}>
<Text style={current === value ? palPrimary.text : pal.text}>
{label}
</Text>

View file

@ -86,7 +86,10 @@ export function Component({}: {}) {
<>
<TouchableOpacity
style={styles.mt20}
onPress={onPressSendEmail}>
onPress={onPressSendEmail}
accessibilityRole="button"
accessibilityLabel="Send email"
accessibilityHint="Sends email with confirmation code for account deletion">
<LinearGradient
colors={[
gradients.blueLight.start,
@ -102,7 +105,11 @@ export function Component({}: {}) {
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, s.mt10]}
onPress={onCancel}>
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel account deletion"
accessibilityHint=""
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
</Text>
@ -112,7 +119,11 @@ export function Component({}: {}) {
</>
) : (
<>
<Text type="lg" style={styles.description}>
{/* TODO: Update this label to be more concise */}
<Text
type="lg"
style={styles.description}
nativeID="confirmationCode">
Check your inbox for an email with the confirmation code to enter
below:
</Text>
@ -123,8 +134,11 @@ export function Component({}: {}) {
keyboardAppearance={theme.colorScheme}
value={confirmCode}
onChangeText={setConfirmCode}
accessibilityLabelledBy="confirmationCode"
accessibilityLabel="Confirmation code"
accessibilityHint="Input confirmation code for account deletion"
/>
<Text type="lg" style={styles.description}>
<Text type="lg" style={styles.description} nativeID="password">
Please enter your password as well:
</Text>
<TextInput
@ -135,6 +149,9 @@ export function Component({}: {}) {
secureTextEntry
value={password}
onChangeText={setPassword}
accessibilityLabelledBy="password"
accessibilityLabel="Password"
accessibilityHint="Input password for account deletion"
/>
{error ? (
<View style={styles.mt20}>
@ -149,14 +166,21 @@ export function Component({}: {}) {
<>
<TouchableOpacity
style={[styles.btn, styles.evilBtn, styles.mt20]}
onPress={onPressConfirmDelete}>
onPress={onPressConfirmDelete}
accessibilityRole="button"
accessibilityLabel="Confirm delete account"
accessibilityHint="">
<Text type="button-lg" style={[s.white, s.bold]}>
Delete my account
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, s.mt10]}
onPress={onCancel}>
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel account deletion"
accessibilityHint="Exits account deletion process"
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
</Text>

View file

@ -175,6 +175,9 @@ export function Component({
onChangeText={v =>
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
}
accessible={true}
accessibilityLabel="Display name"
accessibilityHint="Edit your display name"
/>
</View>
<View style={s.pb10}>
@ -188,6 +191,9 @@ export function Component({
multiline
value={description}
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
accessible={true}
accessibilityLabel="Description"
accessibilityHint="Edit your profile description"
/>
</View>
{isProcessing ? (
@ -198,7 +204,10 @@ export function Component({
<TouchableOpacity
testID="editProfileSaveBtn"
style={s.mt10}
onPress={onPressSave}>
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityHint="Saves any changes to your profile">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
@ -211,7 +220,11 @@ export function Component({
<TouchableOpacity
testID="editProfileCancelBtn"
style={s.mt5}
onPress={onPressCancel}>
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel profile editing"
accessibilityHint=""
onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
</View>

View file

@ -87,6 +87,7 @@ const InviteCode = observer(
({testID, code, used}: {testID: string; code: string; used?: boolean}) => {
const pal = usePalette('default')
const store = useStores()
const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
Clipboard.setString(code)
@ -98,7 +99,14 @@ const InviteCode = observer(
<TouchableOpacity
testID={testID}
style={[styles.inviteCode, pal.border]}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={
invitesAvailable === 1
? 'Invite codes: 1 available'
: `Invite codes: ${invitesAvailable} available`
}
accessibilityHint="Opens list of invite codes">
<Text
testID={`${testID}-code`}
type={used ? 'md' : 'md-bold'}

View file

@ -53,6 +53,7 @@ function Modal({modal}: {modal: ModalIface}) {
store.shell.closeModal()
}
const onInnerPress = () => {
// TODO: can we use prevent default?
// do nothing, we just want to stop it from bubbling
}
@ -92,8 +93,10 @@ function Modal({modal}: {modal: ModalIface}) {
}
return (
// eslint-disable-next-line
<TouchableWithoutFeedback onPress={onPressMask}>
<View style={styles.mask}>
{/* eslint-disable-next-line */}
<TouchableWithoutFeedback onPress={onInnerPress}>
<View
style={[

View file

@ -110,7 +110,10 @@ export function Component({did}: {did: string}) {
<TouchableOpacity
testID="sendReportBtn"
style={s.mt10}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Report account"
accessibilityHint={`Reports account with reason ${issue}`}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View file

@ -153,7 +153,10 @@ export function Component({
<TouchableOpacity
testID="sendReportBtn"
style={s.mt10}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Report post"
accessibilityHint={`Reports post with reason ${issue}`}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View file

@ -18,6 +18,7 @@ export function Component({
onRepost: () => void
onQuote: () => void
isReposted: boolean
// TODO: Add author into component
}) {
const store = useStores()
const pal = usePalette('default')
@ -31,7 +32,10 @@ export function Component({
<TouchableOpacity
testID="repostBtn"
style={[styles.actionBtn]}
onPress={onRepost}>
onPress={onRepost}
accessibilityRole="button"
accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'}
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
{!isReposted ? 'Repost' : 'Undo repost'}
@ -40,14 +44,23 @@ export function Component({
<TouchableOpacity
testID="quoteBtn"
style={[styles.actionBtn]}
onPress={onQuote}>
onPress={onQuote}
accessibilityRole="button"
accessibilityLabel="Quote post"
accessibilityHint="">
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
Quote Post
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity testID="cancelBtn" onPress={onPress}>
<TouchableOpacity
testID="cancelBtn"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Cancel quote post"
accessibilityHint=""
onAccessibilityEscape={onPress}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View file

@ -41,7 +41,8 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
<TouchableOpacity
testID="localDevServerButton"
style={styles.btn}
onPress={() => doSelect(LOCAL_DEV_SERVICE)}>
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>Local dev server</Text>
<FontAwesomeIcon
icon="arrow-right"
@ -50,7 +51,8 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
</TouchableOpacity>
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(STAGING_SERVICE)}>
onPress={() => doSelect(STAGING_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>Staging</Text>
<FontAwesomeIcon
icon="arrow-right"
@ -61,7 +63,10 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
) : undefined}
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(PROD_SERVICE)}>
onPress={() => doSelect(PROD_SERVICE)}
accessibilityRole="button"
accessibilityLabel="Select Bluesky Social"
accessibilityHint="Sets Bluesky Social as your service provider">
<Text style={styles.btnText}>Bluesky.Social</Text>
<FontAwesomeIcon
icon="arrow-right"
@ -83,11 +88,23 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
keyboardAppearance={theme.colorScheme}
value={customUrl}
onChangeText={setCustomUrl}
accessibilityLabel="Custom domain"
// TODO: Simplify this wording further to be understandable by everyone
accessibilityHint="Use your domain as your Bluesky client service provider"
/>
<TouchableOpacity
testID="customServerSelectBtn"
style={[pal.borderDark, pal.text, styles.textInputBtn]}
onPress={() => doSelect(customUrl)}>
onPress={() => doSelect(customUrl)}
accessibilityRole="button"
accessibilityLabel={`Confirm service. ${
customUrl === ''
? 'Button disabled. Input custom domain to proceed.'
: ''
}`}
accessibilityHint=""
// TODO - accessibility: Need to inform state change on failure
disabled={customUrl === ''}>
<FontAwesomeIcon
icon="check"
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}

View file

@ -77,6 +77,9 @@ export function Component({}: {}) {
keyboardAppearance={theme.colorScheme}
value={email}
onChangeText={setEmail}
accessible={true}
accessibilityLabel="Email"
accessibilityHint="Input your email to get on the Bluesky waitlist"
/>
{error ? (
<View style={s.mt10}>
@ -99,7 +102,10 @@ export function Component({}: {}) {
</View>
) : (
<>
<TouchableOpacity onPress={onPressSignup}>
<TouchableOpacity
onPress={onPressSignup}
accessibilityRole="button"
accessibilityHint={`Confirms signing up ${email} to the waitlist`}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
@ -110,7 +116,13 @@ export function Component({}: {}) {
</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, s.mt10]} onPress={onCancel}>
<TouchableOpacity
style={[styles.btn, s.mt10]}
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel waitlist signup"
accessibilityHint={`Exits signing up for waitlist with ${email}`}
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
</Text>

View file

@ -4,12 +4,13 @@ import ImageEditor from 'react-avatar-editor'
import {Slider} from '@miblanchard/react-native-slider'
import LinearGradient from 'react-native-linear-gradient'
import {Text} from 'view/com/util/text/Text'
import {Dimensions, Image} from 'lib/media/types'
import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {s, gradients} from 'lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
import {Image as RNImage} from 'react-native-image-crop-picker'
enum AspectRatio {
Square = 'square',
@ -30,7 +31,7 @@ export function Component({
onSelect,
}: {
uri: string
onSelect: (img?: Image) => void
onSelect: (img?: RNImage) => void
}) {
const store = useStores()
const pal = usePalette('default')
@ -92,19 +93,31 @@ export function Component({
maximumValue={3}
containerStyle={styles.slider}
/>
<TouchableOpacity onPress={doSetAs(AspectRatio.Wide)}>
<TouchableOpacity
onPress={doSetAs(AspectRatio.Wide)}
accessibilityRole="button"
accessibilityLabel="Wide"
accessibilityHint="Sets image aspect ratio to wide">
<RectWideIcon
size={24}
style={as === AspectRatio.Wide ? s.blue3 : undefined}
/>
</TouchableOpacity>
<TouchableOpacity onPress={doSetAs(AspectRatio.Tall)}>
<TouchableOpacity
onPress={doSetAs(AspectRatio.Tall)}
accessibilityRole="button"
accessibilityLabel="Tall"
accessibilityHint="Sets image aspect ratio to tall">
<RectTallIcon
size={24}
style={as === AspectRatio.Tall ? s.blue3 : undefined}
/>
</TouchableOpacity>
<TouchableOpacity onPress={doSetAs(AspectRatio.Square)}>
<TouchableOpacity
onPress={doSetAs(AspectRatio.Square)}
accessibilityRole="button"
accessibilityLabel="Square"
accessibilityHint="Sets image aspect ratio to square">
<SquareIcon
size={24}
style={as === AspectRatio.Square ? s.blue3 : undefined}
@ -112,13 +125,21 @@ export function Component({
</TouchableOpacity>
</View>
<View style={styles.btns}>
<TouchableOpacity onPress={onPressCancel}>
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel image crop"
accessibilityHint="Exits image cropping process">
<Text type="xl" style={pal.link}>
Cancel
</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressDone}>
<TouchableOpacity
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel="Save image crop"
accessibilityHint="Saves image crop settings">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}