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>
zio/stable
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

@ -1,6 +1,6 @@
module.exports = {
root: true,
extends: '@react-native-community',
extends: ['@react-native-community', 'plugin:react-native-a11y/ios'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox'],
ignorePatterns: [

View File

@ -57,8 +57,9 @@
}
}*/
/* OLLIE: TODO -- this is not accessible */
/* Remove focus state on inputs */
*:focus {
input:focus {
outline: 0;
}
/* Remove default link styling */

View File

@ -62,6 +62,7 @@
"await-lock": "^2.2.2",
"base64-js": "^1.5.1",
"email-validator": "^2.0.4",
"eslint-plugin-react-native-a11y": "^3.3.0",
"expo": "~48.0.15",
"expo-application": "~5.1.1",
"expo-build-properties": "~0.5.1",

View File

@ -6,7 +6,7 @@ const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu
export function sanitizeDisplayName(str: string): string {
if (typeof str === 'string') {
return str.replace(CHECK_MARKS_RE, '')
return str.replace(CHECK_MARKS_RE, '').trim()
}
return ''
}

View File

@ -118,6 +118,7 @@ export const s = StyleSheet.create({
mr2: {marginRight: 2},
mr5: {marginRight: 5},
mr10: {marginRight: 10},
mr20: {marginRight: 20},
ml2: {marginLeft: 2},
ml5: {marginLeft: 5},
ml10: {marginLeft: 10},
@ -149,6 +150,7 @@ export const s = StyleSheet.create({
pb5: {paddingBottom: 5},
pb10: {paddingBottom: 10},
pb20: {paddingBottom: 20},
px5: {paddingHorizontal: 5},
// flex
flexRow: {flexDirection: 'row'},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 ? (

View File

@ -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} />

View File

@ -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{' '}

View File

@ -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>

View File

@ -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>
)

View File

@ -7,7 +7,6 @@ import {
ScrollView,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
@ -19,6 +18,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {ExternalEmbed} from './ExternalEmbed'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
// TODO: Prevent naming components that coincide with RN primitives
// due to linting false positives
import {TextInput, TextInputRef} from './text-input/TextInput'
import {CharProgress} from './char-progress/CharProgress'
import {UserAvatar} from '../util/UserAvatar'
@ -87,27 +88,6 @@ export const ComposePost = observer(function ComposePost({
autocompleteView.setup()
}, [autocompleteView])
useEffect(() => {
// HACK
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
// -prf
let to: NodeJS.Timeout | undefined
if (textInput.current) {
to = setTimeout(() => {
textInput.current?.focus()
}, 250)
}
return () => {
if (to) {
clearTimeout(to)
}
}
}, [])
const onPressContainer = useCallback(() => {
textInput.current?.focus()
}, [textInput])
const onPressAddLinkCard = useCallback(
(uri: string) => {
setExtLink({uri, isLoading: true})
@ -133,7 +113,7 @@ export const ComposePost = observer(function ComposePost({
if (rt.text.trim().length === 0 && gallery.isEmpty) {
setError('Did you want to say anything?')
return false
return
}
setIsProcessing(true)
@ -203,133 +183,149 @@ export const ComposePost = observer(function ComposePost({
testID="composePostView"
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.outer}>
<TouchableWithoutFeedback onPressIn={onPressContainer}>
<View style={[s.flex1, viewStyles]}>
<View style={styles.topbar}>
<TouchableOpacity
testID="composerCancelButton"
onPress={hackfixOnClose}>
<Text style={[pal.link, s.f18]}>Cancel</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
) : canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={() => {
onPressPublish(richtext)
}}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? 'Reply' : 'Post'}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
</View>
)}
</View>
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
<View style={styles.topbar}>
<TouchableOpacity
testID="composerCancelButton"
onPress={hackfixOnClose}
onAccessibilityEscape={hackfixOnClose}
accessibilityRole="button"
accessibilityLabel="Cancel"
accessibilityHint="Closes post composer">
<Text style={[pal.link, s.f18]}>Cancel</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<View style={[pal.btn, styles.processingLine]}>
<Text style={pal.text}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
) : undefined}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, s.flex1]}>{error}</Text>
) : canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={() => {
onPressPublish(richtext)
}}
accessibilityRole="button"
accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'}
accessibilityHint={
replyTo
? 'Double tap to publish your reply'
: 'Double tap to publish your post'
}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? 'Reply' : 'Post'}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
</View>
)}
<ScrollView
style={styles.scrollView}
keyboardShouldPersistTaps="always">
{replyTo ? (
<View style={[pal.border, styles.replyToLayout]}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName || replyTo.author.handle,
)}
</Text>
<Text type="post-text" style={pal.text} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
<View style={[pal.border, styles.textInputLayout]}>
<UserAvatar avatar={store.me.avatar} size={50} />
<TextInput
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
suggestedLinks={suggestedLinks}
autocompleteView={autocompleteView}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
onSuggestedLinksChanged={setSuggestedLinks}
onError={setError}
/>
</View>
<Gallery gallery={gallery} />
{gallery.isEmpty && extLink && (
<ExternalEmbed
link={extLink}
onRemove={() => setExtLink(undefined)}
/>
)}
{quote ? (
<View style={s.mt5}>
<QuoteEmbed quote={quote} />
</View>
) : undefined}
</ScrollView>
{!extLink && suggestedLinks.size > 0 ? (
<View style={s.mb5}>
{Array.from(suggestedLinks).map(url => (
<TouchableOpacity
key={`suggested-${url}`}
testID="addLinkCardBtn"
style={[pal.borderDark, styles.addExtLinkBtn]}
onPress={() => onPressAddLinkCard(url)}>
<Text style={pal.text}>
Add link card: <Text style={pal.link}>{url}</Text>
</Text>
</TouchableOpacity>
))}
</View>
) : null}
<View style={[pal.border, styles.bottomBar]}>
{canSelectImages ? (
<>
<SelectPhotoBtn gallery={gallery} />
<OpenCameraBtn gallery={gallery} />
</>
) : null}
<View style={s.flex1} />
<CharProgress count={graphemeLength} />
</View>
</View>
</TouchableWithoutFeedback>
{isProcessing ? (
<View style={[pal.btn, styles.processingLine]}>
<Text style={pal.text}>{processingState}</Text>
</View>
) : undefined}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, s.flex1]}>{error}</Text>
</View>
)}
<ScrollView
style={styles.scrollView}
keyboardShouldPersistTaps="always">
{replyTo ? (
<View style={[pal.border, styles.replyToLayout]}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName || replyTo.author.handle,
)}
</Text>
<Text type="post-text" style={pal.text} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
<View style={[pal.border, styles.textInputLayout]}>
<UserAvatar avatar={store.me.avatar} size={50} />
<TextInput
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
suggestedLinks={suggestedLinks}
autocompleteView={autocompleteView}
autoFocus={true}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
onSuggestedLinksChanged={setSuggestedLinks}
onError={setError}
accessible={true}
accessibilityLabel="Write post"
accessibilityHint="Compose posts up to 300 characters in length"
/>
</View>
<Gallery gallery={gallery} />
{gallery.isEmpty && extLink && (
<ExternalEmbed
link={extLink}
onRemove={() => setExtLink(undefined)}
/>
)}
{quote ? (
<View style={s.mt5}>
<QuoteEmbed quote={quote} />
</View>
) : undefined}
</ScrollView>
{!extLink && suggestedLinks.size > 0 ? (
<View style={s.mb5}>
{Array.from(suggestedLinks).map(url => (
<TouchableOpacity
key={`suggested-${url}`}
testID="addLinkCardBtn"
style={[pal.borderDark, styles.addExtLinkBtn]}
onPress={() => onPressAddLinkCard(url)}
accessibilityRole="button"
accessibilityLabel="Add link card"
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
<Text style={pal.text}>
Add link card: <Text style={pal.link}>{url}</Text>
</Text>
</TouchableOpacity>
))}
</View>
) : null}
<View style={[pal.border, styles.bottomBar]}>
{canSelectImages ? (
<>
<SelectPhotoBtn gallery={gallery} />
<OpenCameraBtn gallery={gallery} />
</>
) : null}
<View style={s.flex1} />
<CharProgress count={graphemeLength} />
</View>
</View>
</KeyboardAvoidingView>
)
})

View File

@ -60,7 +60,13 @@ export const ExternalEmbed = ({
</Text>
)}
</View>
<TouchableOpacity style={styles.removeBtn} onPress={onRemove}>
<TouchableOpacity
style={styles.removeBtn}
onPress={onRemove}
accessibilityRole="button"
accessibilityLabel="Remove image preview"
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
onAccessibilityEscape={onRemove}>
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
</TouchableOpacity>
</View>

View File

@ -13,7 +13,10 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
<TouchableOpacity
testID="replyPromptBtn"
style={[pal.view, pal.border, styles.prompt]}
onPress={() => onPressCompose()}>
onPress={() => onPressCompose()}
accessibilityRole="button"
accessibilityLabel="Compose reply"
accessibilityHint="Opens composer">
<UserAvatar avatar={store.me.avatar} size={38} />
<Text
type="xl"

View File

@ -107,6 +107,9 @@ export const Gallery = observer(function ({gallery}: Props) {
<View key={`selected-image-${image.path}`} style={[imageStyle]}>
<TouchableOpacity
testID="altTextButton"
accessibilityRole="button"
accessibilityLabel="Add alt text"
accessibilityHint="Opens modal for inputting image alt text"
onPress={() => {
handleAddImageAltText(image)
}}
@ -116,6 +119,9 @@ export const Gallery = observer(function ({gallery}: Props) {
<View style={imageControlsSubgroupStyle}>
<TouchableOpacity
testID="cropPhotoButton"
accessibilityRole="button"
accessibilityLabel="Crop image"
accessibilityHint="Opens modal for cropping image"
onPress={() => {
handleEditPhoto(image)
}}
@ -128,6 +134,9 @@ export const Gallery = observer(function ({gallery}: Props) {
</TouchableOpacity>
<TouchableOpacity
testID="removePhotoButton"
accessibilityRole="button"
accessibilityLabel="Remove image"
accessibilityHint=""
onPress={() => handleRemovePhoto(image)}
style={styles.imageControl}>
<FontAwesomeIcon
@ -144,6 +153,8 @@ export const Gallery = observer(function ({gallery}: Props) {
source={{
uri: image.compressed.path,
}}
accessible={true}
accessibilityIgnoresInvertColors
/>
</View>
) : null,

View File

@ -1,5 +1,5 @@
import React, {useCallback} from 'react'
import {TouchableOpacity} from 'react-native'
import {TouchableOpacity, StyleSheet} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -7,7 +7,6 @@ import {
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
import {isDesktopWeb} from 'platform/detection'
import {openCamera} from 'lib/media/picker'
import {useCameraPermission} from 'lib/hooks/usePermissions'
@ -54,8 +53,11 @@ export function OpenCameraBtn({gallery}: Props) {
<TouchableOpacity
testID="openCameraButton"
onPress={onPressTakePicture}
style={[s.pl5]}
hitSlop={HITSLOP}>
style={styles.button}
hitSlop={HITSLOP}
accessibilityRole="button"
accessibilityLabel="Camera"
accessibilityHint="Opens camera on device">
<FontAwesomeIcon
icon="camera"
style={pal.link as FontAwesomeIconStyle}
@ -64,3 +66,9 @@ export function OpenCameraBtn({gallery}: Props) {
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
button: {
paddingHorizontal: 15,
},
})

View File

@ -1,12 +1,11 @@
import React, {useCallback} from 'react'
import {TouchableOpacity} from 'react-native'
import {TouchableOpacity, StyleSheet} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {s} from 'lib/styles'
import {isDesktopWeb} from 'platform/detection'
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
import {GalleryModel} from 'state/models/media/gallery'
@ -36,8 +35,11 @@ export function SelectPhotoBtn({gallery}: Props) {
<TouchableOpacity
testID="openGalleryBtn"
onPress={onPressSelectPhotos}
style={[s.pl5, s.pr20]}
hitSlop={HITSLOP}>
style={styles.button}
hitSlop={HITSLOP}
accessibilityRole="button"
accessibilityLabel="Gallery"
accessibilityHint="Opens device photo gallery">
<FontAwesomeIcon
icon={['far', 'image']}
style={pal.link as FontAwesomeIconStyle}
@ -46,3 +48,9 @@ export function SelectPhotoBtn({gallery}: Props) {
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
button: {
paddingHorizontal: 15,
},
})

View File

@ -1,7 +1,14 @@
import React, {forwardRef, useCallback, useEffect, useRef, useMemo} from 'react'
import React, {
forwardRef,
useCallback,
useRef,
useMemo,
ComponentProps,
} from 'react'
import {
NativeSyntheticEvent,
StyleSheet,
TextInput as RNTextInput,
TextInputSelectionChangeEventData,
View,
} from 'react-native'
@ -27,14 +34,14 @@ export interface TextInputRef {
blur: () => void
}
interface TextInputProps {
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
richtext: RichText
placeholder: string
suggestedLinks: Set<string>
autocompleteView: UserAutocompleteModel
setRichText: (v: RichText) => void
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<false | undefined>
onPressPublish: (richtext: RichText) => Promise<void>
onSuggestedLinksChanged: (uris: Set<string>) => void
onError: (err: string) => void
}
@ -55,6 +62,7 @@ export const TextInput = forwardRef(
onPhotoPasted,
onSuggestedLinksChanged,
onError,
...props
}: TextInputProps,
ref,
) => {
@ -65,26 +73,11 @@ export const TextInput = forwardRef(
React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(),
blur: () => textInput.current?.blur(),
blur: () => {
textInput.current?.blur()
},
}))
useEffect(() => {
// HACK
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
// -prf
let to: NodeJS.Timeout | undefined
if (textInput.current) {
to = setTimeout(() => {
textInput.current?.focus()
}, 250)
}
return () => {
if (to) {
clearTimeout(to)
}
}
}, [])
const onChangeText = useCallback(
async (newText: string) => {
const newRt = new RichText({text: newText})
@ -206,8 +199,10 @@ export const TextInput = forwardRef(
placeholder={placeholder}
placeholderTextColor={pal.colors.textLight}
keyboardAppearance={theme.colorScheme}
autoFocus={true}
multiline
style={[pal.text, styles.textInput, styles.textInputFormatting]}>
style={[pal.text, styles.textInput, styles.textInputFormatting]}
{...props}>
{textDecorated}
</PasteInput>
<Autocomplete

View File

@ -25,9 +25,9 @@ interface TextInputProps {
placeholder: string
suggestedLinks: Set<string>
autocompleteView: UserAutocompleteModel
setRichText: (v: RichText) => void
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<false | undefined>
onPressPublish: (richtext: RichText) => Promise<void>
onSuggestedLinksChanged: (uris: Set<string>) => void
onError: (err: string) => void
}

View File

@ -50,7 +50,9 @@ export const Autocomplete = observer(
testID="autocompleteButton"
key={item.handle}
style={[pal.border, styles.item]}
onPress={() => onSelect(item.handle)}>
onPress={() => onSelect(item.handle)}
accessibilityLabel={`Select ${item.handle}`}
accessibilityHint={`Autocompletes to ${item.handle}`}>
<Text type="md-medium" style={pal.text}>
{item.displayName || item.handle}
<Text type="sm" style={pal.textLight}>

View File

@ -20,7 +20,11 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
<TouchableOpacity
style={styles.closeButton}
onPress={onRequestClose}
hitSlop={HIT_SLOP}>
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel="Close image"
accessibilityHint="Closes viewer for header image"
onAccessibilityEscape={onRequestClose}>
<Text style={styles.closeText}></Text>
</TouchableOpacity>
</SafeAreaView>

View File

@ -127,7 +127,8 @@ const ImageItem = ({
<TouchableWithoutFeedback
onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
onLongPress={onLongPressHandler}
delayLongPress={delayLongPress}>
delayLongPress={delayLongPress}
accessibilityRole="image">
<Animated.Image
source={imageSrc}
style={imageStylesWithOpacity}

View File

@ -112,7 +112,12 @@ function ImageViewing({
}
return (
<SafeAreaView style={styles.screen} onLayout={onLayout} edges={edges}>
<SafeAreaView
style={styles.screen}
onLayout={onLayout}
edges={edges}
aria-modal
accessibilityViewIsModal>
<ModalsContainer />
<View style={[styles.container, {opacity, backgroundColor}]}>
<Animated.View style={[styles.header, {transform: headerTransform}]}>

View File

@ -89,13 +89,25 @@ function LightboxInner({
return (
<View style={styles.mask}>
<TouchableWithoutFeedback onPress={onClose}>
<TouchableWithoutFeedback
onPress={onClose}
accessibilityRole="button"
accessibilityLabel="Close image viewer"
accessibilityHint="Exits image view"
onAccessibilityEscape={onClose}>
<View style={styles.imageCenterer}>
<Image source={imgs[index]} style={styles.image} />
<Image
accessibilityIgnoresInvertColors
source={imgs[index]}
style={styles.image}
/>
{canGoLeft && (
<TouchableOpacity
onPress={onPressLeft}
style={[styles.btn, styles.leftBtn]}>
style={[styles.btn, styles.leftBtn]}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to previous image in viewer">
<FontAwesomeIcon
icon="angle-left"
style={styles.icon}
@ -106,7 +118,10 @@ function LightboxInner({
{canGoRight && (
<TouchableOpacity
onPress={onPressRight}
style={[styles.btn, styles.rightBtn]}>
style={[styles.btn, styles.rightBtn]}
accessibilityRole="button"
accessibilityLabel="Go to next"
accessibilityHint="Navigates to next image in viewer">
<FontAwesomeIcon
icon="angle-right"
style={styles.icon}

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}}

View File

@ -123,7 +123,8 @@ export const FeedItem = observer(function ({
testID={`feedItem-by-${item.author.handle}`}
href={itemHref}
title={itemTitle}
noFeedback>
noFeedback
accessible={false}>
<Post
uri={item.uri}
initView={item.additionalPost}
@ -163,6 +164,7 @@ export const FeedItem = observer(function ({
}
return (
// eslint-disable-next-line
<Link
testID={`feedItem-by-${item.author.handle}`}
style={[
@ -178,8 +180,11 @@ export const FeedItem = observer(function ({
]}
href={itemHref}
title={itemTitle}
noFeedback>
noFeedback
accessible={(item.isLike && authors.length === 1) || item.isRepost}>
<View style={styles.layoutIcon}>
{/* TODO: Prevent conditional rendering and move toward composable
notifications for clearer accessibility labeling */}
{icon === 'HeartIconSolid' ? (
<HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} />
) : (
@ -192,17 +197,18 @@ export const FeedItem = observer(function ({
</View>
<View style={styles.layoutContent}>
<Pressable
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}>
onPress={authors.length > 1 ? onToggleAuthorsExpanded : undefined}
accessible={false}>
<CondensedAuthorsList
visible={!isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
/>
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
<View style={styles.meta}>
<Text style={styles.meta}>
<TextLink
key={authors[0].href}
style={[pal.text, s.bold, styles.metaItem]}
style={[pal.text, s.bold]}
href={authors[0].href}
text={sanitizeDisplayName(
authors[0].displayName || authors[0].handle,
@ -210,17 +216,15 @@ export const FeedItem = observer(function ({
/>
{authors.length > 1 ? (
<>
<Text style={[styles.metaItem, pal.text]}>and</Text>
<Text style={[styles.metaItem, pal.text, s.bold]}>
<Text style={[pal.text]}> and </Text>
<Text style={[pal.text, s.bold]}>
{authors.length - 1} {pluralize(authors.length - 1, 'other')}
</Text>
</>
) : undefined}
<Text style={[styles.metaItem, pal.text]}>{action}</Text>
<Text style={[styles.metaItem, pal.textLight]}>
{ago(item.indexedAt)}
</Text>
</View>
<Text style={[pal.text]}> {action}</Text>
<Text style={[pal.textLight]}> {ago(item.indexedAt)}</Text>
</Text>
</Pressable>
{item.isLike || item.isRepost || item.isQuote ? (
<AdditionalPostText additionalPost={item.additionalPost} />
@ -245,7 +249,10 @@ function CondensedAuthorsList({
<View style={styles.avis}>
<TouchableOpacity
style={styles.expandedAuthorsCloseBtn}
onPress={onToggleAuthorsExpanded}>
onPress={onToggleAuthorsExpanded}
accessibilityRole="button"
accessibilityLabel="Hide user list"
accessibilityHint="Collapses list of users for a given notification">
<FontAwesomeIcon
icon="angle-up"
size={18}
@ -276,27 +283,32 @@ function CondensedAuthorsList({
)
}
return (
<View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => (
<View key={author.href} style={s.mr5}>
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
/>
</View>
))}
{authors.length > MAX_AUTHORS ? (
<Text style={[styles.aviExtraCount, pal.textLight]}>
+{authors.length - MAX_AUTHORS}
</Text>
) : undefined}
<FontAwesomeIcon
icon="angle-down"
size={18}
style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]}
/>
</View>
<TouchableOpacity
accessibilityLabel="Show users"
accessibilityHint="Opens an expanded list of users in this notification"
onPress={onToggleAuthorsExpanded}>
<View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => (
<View key={author.href} style={s.mr5}>
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
/>
</View>
))}
{authors.length > MAX_AUTHORS ? (
<Text style={[styles.aviExtraCount, pal.textLight]}>
+{authors.length - MAX_AUTHORS}
</Text>
) : undefined}
<FontAwesomeIcon
icon="angle-down"
size={18}
style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]}
/>
</View>
</TouchableOpacity>
)
}
@ -426,9 +438,6 @@ const styles = StyleSheet.create({
paddingTop: 6,
paddingBottom: 2,
},
metaItem: {
paddingRight: 3,
},
postText: {
paddingBottom: 5,
color: colors.black,

View File

@ -37,7 +37,10 @@ export const FeedsTabBar = observer(
<TouchableOpacity
testID="viewHeaderDrawerBtn"
style={styles.tabBarAvi}
onPress={onPressAvi}>
onPress={onPressAvi}
accessibilityRole="button"
accessibilityLabel="Open navigation"
accessibilityHint="Access profile and other navigation links">
<UserAvatar avatar={store.me.avatar} size={30} />
</TouchableOpacity>
<TabBar

View File

@ -180,7 +180,11 @@ export const PostThread = observer(function PostThread({
<Text type="md" style={[pal.text, s.mb10]}>
The post may have been deleted.
</Text>
<TouchableOpacity onPress={onPressBack}>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
icon="angle-left"
@ -210,7 +214,11 @@ export const PostThread = observer(function PostThread({
<Text type="md" style={[pal.text, s.mb10]}>
You have blocked the author or you have been blocked by the author.
</Text>
<TouchableOpacity onPress={onPressBack}>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
icon="angle-left"

View File

@ -151,7 +151,12 @@ export const PostThreadItem = observer(function PostThreadItem({
moderation={item.moderation.thread}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle} asAnchor>
<Link
href={authorHref}
title={authorTitle}
asAnchor
accessibilityLabel={`${item.post.author.handle}'s avatar`}
accessibilityHint="">
<UserAvatar
size={52}
avatar={item.post.author.avatar}
@ -183,7 +188,7 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={s.flex1} />
<PostDropdownBtn
testID="postDropdownBtn"
style={styles.metaItem}
style={[styles.metaItem, s.mt2, s.px5]}
itemUri={itemUri}
itemCid={itemCid}
itemHref={itemHref}
@ -197,7 +202,7 @@ export const PostThreadItem = observer(function PostThreadItem({
<FontAwesomeIcon
icon="ellipsis-h"
size={14}
style={[s.mt2, s.mr5, pal.textLight]}
style={[pal.textLight]}
/>
</PostDropdownBtn>
</View>
@ -435,10 +440,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
layoutAvi: {
width: 70,
paddingLeft: 10,
paddingTop: 10,
paddingBottom: 10,
marginRight: 10,
},
layoutContent: {
flex: 1,

View File

@ -282,7 +282,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="profileHeaderEditProfileButton"
onPress={onPressEditProfile}
style={[styles.btn, styles.mainBtn, pal.btn]}>
style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button"
accessibilityLabel="Edit profile"
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
<Text type="button" style={pal.text}>
Edit Profile
</Text>
@ -291,7 +294,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="unblockBtn"
onPress={onPressUnblockAccount}
style={[styles.btn, styles.mainBtn, pal.btn]}>
style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button"
accessibilityLabel="Unblock"
accessibilityHint="">
<Text type="button" style={[pal.text, s.bold]}>
Unblock
</Text>
@ -303,7 +309,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="unfollowBtn"
onPress={onPressToggleFollow}
style={[styles.btn, styles.mainBtn, pal.btn]}>
style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button"
accessibilityLabel={`Unfollow ${view.handle}`}
accessibilityHint={`Hides direct posts from ${view.handle} in your feed`}>
<FontAwesomeIcon
icon="check"
style={[pal.text, s.mr5]}
@ -317,7 +326,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="followBtn"
onPress={onPressToggleFollow}
style={[styles.btn, styles.primaryBtn]}>
style={[styles.btn, styles.primaryBtn]}
accessibilityRole="button"
accessibilityLabel={`Follow ${view.handle}`}
accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
<FontAwesomeIcon
icon="plus"
style={[s.white as FontAwesomeIconStyle, s.mr5]}
@ -363,7 +375,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="profileHeaderFollowersButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollowers}>
onPress={onPressFollowers}
accessibilityRole="button"
accessibilityLabel={`Show ${view.handle}'s followers`}
accessibilityHint={`Shows folks following ${view.handle}`}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{formatCount(view.followersCount)}
</Text>
@ -374,7 +389,10 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity
testID="profileHeaderFollowsButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollows}>
onPress={onPressFollows}
accessibilityRole="button"
accessibilityLabel={`Show ${view.handle}'s follows`}
accessibilityHint={`Shows folks followed by ${view.handle}`}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{formatCount(view.followsCount)}
</Text>
@ -382,14 +400,12 @@ const ProfileHeaderLoaded = observer(
following
</Text>
</TouchableOpacity>
<View style={[s.flexRow, s.mr10]}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{view.postsCount}
</Text>
<Text type="md" style={[s.bold, pal.text]}>
{view.postsCount}{' '}
<Text type="md" style={[pal.textLight]}>
{pluralize(view.postsCount, 'post')}
</Text>
</View>
</Text>
</View>
{view.descriptionRichText ? (
<RichText
@ -440,7 +456,10 @@ const ProfileHeaderLoaded = observer(
{!isDesktopWeb && !hideBackButton && (
<TouchableWithoutFeedback
onPress={onPressBack}
hitSlop={BACK_HITSLOP}>
hitSlop={BACK_HITSLOP}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<View style={styles.backBtnWrapper}>
<BlurView style={styles.backBtn} blurType="dark">
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
@ -450,7 +469,10 @@ const ProfileHeaderLoaded = observer(
)}
<TouchableWithoutFeedback
testID="profileHeaderAviButton"
onPress={onPressAvi}>
onPress={onPressAvi}
accessibilityRole="image"
accessibilityLabel={`View ${view.handle}'s avatar`}
accessibilityHint={`Opens ${view.handle}'s avatar in an image viewer`}>
<View
style={[
pal.view,

View File

@ -54,7 +54,9 @@ export function HeaderWithInput({
testID="viewHeaderBackOrMenuBtn"
onPress={onPressMenu}
hitSlop={MENU_HITSLOP}
style={styles.headerMenuBtn}>
style={styles.headerMenuBtn}
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<UserAvatar size={30} avatar={store.me.avatar} />
</TouchableOpacity>
<View
@ -80,9 +82,15 @@ export function HeaderWithInput({
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmitQuery}
autoFocus={true}
accessibilityRole="search"
/>
{query ? (
<TouchableOpacity onPress={onPressClearQuery}>
<TouchableOpacity
onPress={onPressClearQuery}
accessibilityRole="button"
accessibilityLabel="Clear search query"
accessibilityHint="">
<FontAwesomeIcon
icon="xmark"
size={16}
@ -93,7 +101,9 @@ export function HeaderWithInput({
</View>
{query || isInputFocused ? (
<View style={styles.headerCancelBtn}>
<TouchableOpacity onPress={onPressCancelSearchInner}>
<TouchableOpacity
onPress={onPressCancelSearchInner}
accessibilityRole="button">
<Text style={pal.text}>Cancel</Text>
</TouchableOpacity>
</View>
@ -110,9 +120,10 @@ const styles = StyleSheet.create({
paddingVertical: 4,
},
headerMenuBtn: {
width: 40,
width: 30,
height: 30,
marginLeft: 6,
borderRadius: 30,
marginHorizontal: 6,
},
headerSearchContainer: {
flex: 1,

View File

@ -1,5 +1,5 @@
import React, {useMemo} from 'react'
import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native'
import {TouchableWithoutFeedback} from 'react-native'
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
import Animated, {
Extrapolate,
@ -8,7 +8,7 @@ import Animated, {
} from 'react-native-reanimated'
export function createCustomBackdrop(
onClose?: ((event: GestureResponderEvent) => void) | undefined,
onClose?: (() => void) | undefined,
): React.FC<BottomSheetBackdropProps> {
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
// animated variables
@ -27,7 +27,15 @@ export function createCustomBackdrop(
)
return (
<TouchableWithoutFeedback onPress={onClose}>
<TouchableWithoutFeedback
onPress={onClose}
accessibilityLabel="Close bottom drawer"
accessibilityHint=""
onAccessibilityEscape={() => {
if (onClose !== undefined) {
onClose()
}
}}>
<Animated.View style={containerStyle} />
</TouchableWithoutFeedback>
)

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {observer} from 'mobx-react-lite'
import {
Linking,
@ -29,6 +29,16 @@ type Event =
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
| GestureResponderEvent
interface Props extends ComponentProps<typeof TouchableOpacity> {
testID?: string
style?: StyleProp<ViewStyle>
href?: string
title?: string
children?: React.ReactNode
noFeedback?: boolean
asAnchor?: boolean
}
export const Link = observer(function Link({
testID,
style,
@ -37,15 +47,9 @@ export const Link = observer(function Link({
children,
noFeedback,
asAnchor,
}: {
testID?: string
style?: StyleProp<ViewStyle>
href?: string
title?: string
children?: React.ReactNode
noFeedback?: boolean
asAnchor?: boolean
}) {
accessible,
...props
}: Props) {
const store = useStores()
const navigation = useNavigation<NavigationProp>()
@ -64,7 +68,10 @@ export const Link = observer(function Link({
testID={testID}
onPress={onPress}
// @ts-ignore web only -prf
href={asAnchor ? sanitizeUrl(href) : undefined}>
href={asAnchor ? sanitizeUrl(href) : undefined}
accessible={accessible}
accessibilityRole="link"
{...props}>
<View style={style}>
{children ? children : <Text>{title || 'link'}</Text>}
</View>
@ -76,8 +83,11 @@ export const Link = observer(function Link({
testID={testID}
style={style}
onPress={onPress}
accessible={accessible}
accessibilityRole="link"
// @ts-ignore web only -prf
href={asAnchor ? sanitizeUrl(href) : undefined}>
href={asAnchor ? sanitizeUrl(href) : undefined}
{...props}>
{children ? children : <Text>{title || 'link'}</Text>}
</TouchableOpacity>
)

View File

@ -1,157 +0,0 @@
// TODO: replaceme with something in the design system
import React, {useRef} from 'react'
import {
StyleProp,
StyleSheet,
TextStyle,
TouchableOpacity,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import RootSiblings from 'react-native-root-siblings'
import {Text} from './text/Text'
import {colors} from 'lib/styles'
interface PickerItem {
value: string
label: string
}
interface PickerOpts {
style?: StyleProp<ViewStyle>
labelStyle?: StyleProp<TextStyle>
iconStyle?: FontAwesomeIconStyle
items: PickerItem[]
value: string
onChange: (value: string) => void
enabled?: boolean
}
const MENU_WIDTH = 200
export function Picker({
style,
labelStyle,
iconStyle,
items,
value,
onChange,
enabled,
}: PickerOpts) {
const ref = useRef<View>(null)
const valueLabel = items.find(item => item.value === value)?.label || value
const onPress = () => {
if (!enabled) {
return
}
ref.current?.measure(
(
_x: number,
_y: number,
width: number,
height: number,
pageX: number,
pageY: number,
) => {
createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange)
},
)
}
return (
<TouchableWithoutFeedback onPress={onPress}>
<View style={[styles.outer, style]} ref={ref}>
<View style={styles.label}>
<Text style={labelStyle}>{valueLabel}</Text>
</View>
<FontAwesomeIcon icon="angle-down" style={[styles.icon, iconStyle]} />
</View>
</TouchableWithoutFeedback>
)
}
function createDropdownMenu(
x: number,
y: number,
width: number,
items: PickerItem[],
onChange: (value: string) => void,
): RootSiblings {
const onPressItem = (index: number) => {
sibling.destroy()
onChange(items[index].value)
}
const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings(
(
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={styles.bg} />
</TouchableWithoutFeedback>
<View style={[styles.menu, {left: x, top: y, width}]}>
{items.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.menuItem, index !== 0 && styles.menuItemBorder]}
onPress={() => onPressItem(index)}>
<Text style={styles.menuItemLabel}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
</>
),
)
return sibling
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
},
label: {
marginRight: 5,
},
icon: {},
bg: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: '#000',
opacity: 0.1,
},
menu: {
position: 'absolute',
backgroundColor: '#fff',
borderRadius: 14,
opacity: 1,
paddingVertical: 6,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 6,
paddingLeft: 15,
paddingRight: 30,
},
menuItemBorder: {
borderTopWidth: 1,
borderTopColor: colors.gray2,
marginTop: 4,
paddingTop: 12,
},
menuItemIcon: {
marginLeft: 6,
marginRight: 8,
},
menuItemLabel: {
fontSize: 15,
},
})

View File

@ -170,83 +170,94 @@ export function PostCtrls(opts: PostCtrlsOpts) {
return (
<View style={[styles.ctrls, opts.style]}>
<View>
<TouchableOpacity
testID="replyBtn"
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={opts.onPressReply}>
<CommentBottomArrow
style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
strokeWidth={3}
size={opts.big ? 20 : 15}
/>
{typeof opts.replyCount !== 'undefined' ? (
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
{opts.replyCount}
</Text>
) : undefined}
</TouchableOpacity>
</View>
<View>
<TouchableOpacity
testID="repostBtn"
hitSlop={HITSLOP}
onPress={onPressToggleRepostWrapper}
style={styles.ctrl}>
<RepostIcon
<TouchableOpacity
testID="replyBtn"
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={opts.onPressReply}
accessibilityRole="button"
accessibilityLabel="Reply"
accessibilityHint="Opens reply composer">
<CommentBottomArrow
style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
strokeWidth={3}
size={opts.big ? 20 : 15}
/>
{typeof opts.replyCount !== 'undefined' ? (
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
{opts.replyCount}
</Text>
) : undefined}
</TouchableOpacity>
<TouchableOpacity
testID="repostBtn"
hitSlop={HITSLOP}
onPress={onPressToggleRepostWrapper}
style={styles.ctrl}
accessibilityRole="button"
accessibilityLabel={opts.isReposted ? 'Undo repost' : 'Repost'}
accessibilityHint={
opts.isReposted
? `Remove your repost of ${opts.author}'s post`
: `Repost or quote post ${opts.author}'s post`
}>
<RepostIcon
style={
opts.isReposted
? (styles.ctrlIconReposted as StyleProp<ViewStyle>)
: defaultCtrlColor
}
strokeWidth={2.4}
size={opts.big ? 24 : 20}
/>
{typeof opts.repostCount !== 'undefined' ? (
<Text
testID="repostCount"
style={
opts.isReposted
? (styles.ctrlIconReposted as StyleProp<ViewStyle>)
: defaultCtrlColor
}
strokeWidth={2.4}
size={opts.big ? 24 : 20}
? [s.bold, s.green3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.repostCount}
</Text>
) : undefined}
</TouchableOpacity>
<TouchableOpacity
testID="likeBtn"
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={onPressToggleLikeWrapper}
accessibilityRole="button"
accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'}
accessibilityHint={
opts.isReposted
? `Removes like from ${opts.author}'s post`
: `Like ${opts.author}'s post`
}>
{opts.isLiked ? (
<HeartIconSolid
style={styles.ctrlIconLiked as StyleProp<ViewStyle>}
size={opts.big ? 22 : 16}
/>
{typeof opts.repostCount !== 'undefined' ? (
<Text
testID="repostCount"
style={
opts.isReposted
? [s.bold, s.green3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.repostCount}
</Text>
) : undefined}
</TouchableOpacity>
</View>
<View>
<TouchableOpacity
testID="likeBtn"
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={onPressToggleLikeWrapper}>
{opts.isLiked ? (
<HeartIconSolid
style={styles.ctrlIconLiked as StyleProp<ViewStyle>}
size={opts.big ? 22 : 16}
/>
) : (
<HeartIcon
style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
strokeWidth={3}
size={opts.big ? 20 : 16}
/>
)}
{typeof opts.likeCount !== 'undefined' ? (
<Text
testID="likeCount"
style={
opts.isLiked
? [s.bold, s.red3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.likeCount}
</Text>
) : undefined}
</TouchableOpacity>
</View>
) : (
<HeartIcon
style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
strokeWidth={3}
size={opts.big ? 20 : 16}
/>
)}
{typeof opts.likeCount !== 'undefined' ? (
<Text
testID="likeCount"
style={
opts.isLiked
? [s.bold, s.red3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.likeCount}
</Text>
) : undefined}
</TouchableOpacity>
<View>
{opts.big ? undefined : (
<PostDropdownBtn

View File

@ -85,6 +85,8 @@ export function Selector({
onSelect?.(index)
}
const numItems = items.length
return (
<View
style={[pal.view, styles.outer]}
@ -97,7 +99,9 @@ export function Selector({
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}>
onPress={() => onPressItem(i)}
accessibilityLabel={`Select ${item}`}
accessibilityHint={`Select option ${i} of ${numItems}`}>
<View style={styles.item} ref={itemRefs[i]}>
<Text
style={

View File

@ -150,6 +150,7 @@ export function UserAvatar({
borderRadius: Math.floor(size / 2),
}}
source={{uri: avatar}}
accessibilityRole="image"
/>
) : (
<DefaultAvatar size={size} />
@ -167,7 +168,11 @@ export function UserAvatar({
<View style={{width: size, height: size}}>
<HighPriorityImage
testID="userAvatarImage"
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
style={{
width: size,
height: size,
borderRadius: Math.floor(size / 2),
}}
contentFit="cover"
source={{uri: avatar}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}

View File

@ -5,7 +5,6 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {Image} from 'expo-image'
import {colors} from 'lib/styles'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
import {Image as TImage} from 'lib/media/types'
import {useStores} from 'state/index'
import {
usePhotoLibraryPermission,
@ -15,6 +14,7 @@ import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
import {AvatarModeration} from 'lib/labeling/types'
import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
export function UserBanner({
banner,
@ -23,7 +23,7 @@ export function UserBanner({
}: {
banner?: string | null
moderation?: AvatarModeration
onSelectNewBanner?: (img: TImage | null) => void
onSelectNewBanner?: (img: RNImage | null) => void
}) {
const store = useStores()
const pal = usePalette('default')
@ -94,6 +94,8 @@ export function UserBanner({
testID="userBannerImage"
style={styles.bannerImage}
source={{uri: banner}}
accessible={true}
accessibilityIgnoresInvertColors
/>
) : (
<View
@ -118,6 +120,8 @@ export function UserBanner({
resizeMode="cover"
source={{uri: banner}}
blurRadius={moderation?.blur ? 100 : 0}
accessible={true}
accessibilityIgnoresInvertColors
/>
) : (
<View

View File

@ -60,7 +60,14 @@ export const ViewHeader = observer(function ({
testID="viewHeaderDrawerBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}>
style={canGoBack ? styles.backBtn : styles.backBtnWide}
accessibilityRole="button"
accessibilityLabel={canGoBack ? 'Go back' : 'Go to menu'}
accessibilityHint={
canGoBack
? 'Navigates to the previous screen'
: 'Navigates to the menu'
}>
{canGoBack ? (
<FontAwesomeIcon
size={18}
@ -171,9 +178,9 @@ const styles = StyleSheet.create({
height: 30,
},
backBtnWide: {
width: 40,
width: 30,
height: 30,
marginLeft: 6,
paddingHorizontal: 6,
},
backIcon: {
marginTop: 6,

View File

@ -132,7 +132,12 @@ export function Selector({
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}>
onPress={() => onPressItem(i)}
accessibilityLabel={item}
accessibilityHint={`Selects ${item}`}
// TODO: Modify the component API such that lint fails
// at the invocation site as well
>
<View
style={[
styles.item,

View File

@ -47,7 +47,10 @@ export function ErrorMessage({
<TouchableOpacity
testID="errorMessageTryAgainButton"
style={styles.btn}
onPress={onPressTryAgain}>
onPress={onPressTryAgain}
accessibilityRole="button"
accessibilityLabel="Retry"
accessibilityHint="Retries the last action, which errored out">
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: theme.palette.error.icon}}

View File

@ -57,7 +57,9 @@ export function ErrorScreen({
testID="errorScreenTryAgainButton"
type="default"
style={[styles.btn]}
onPress={onPressTryAgain}>
onPress={onPressTryAgain}
accessibilityLabel="Retry"
accessibilityHint="Retries the last action, which errored out">
<FontAwesomeIcon
icon="arrows-rotate"
style={pal.link as FontAwesomeIconStyle}

View File

@ -1,25 +1,19 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
GestureResponderEvent,
StyleSheet,
TouchableWithoutFeedback,
} from 'react-native'
import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {gradients} from 'lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useStores} from 'state/index'
import {isMobileWeb} from 'platform/detection'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export interface FABProps {
export interface FABProps
extends ComponentProps<typeof TouchableWithoutFeedback> {
testID?: string
icon: JSX.Element
onPress: OnPress
}
export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
export const FABInner = observer(({testID, icon, ...props}: FABProps) => {
const store = useStores()
const interp = useAnimatedValue(0)
React.useEffect(() => {
@ -34,7 +28,7 @@ export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
transform: [{translateY: Animated.multiply(interp, 60)}],
}
return (
<TouchableWithoutFeedback testID={testID} onPress={onPress}>
<TouchableWithoutFeedback testID={testID} {...props}>
<Animated.View
style={[styles.outer, isMobileWeb && styles.mobileWebOuter, transform]}>
<LinearGradient

View File

@ -26,6 +26,7 @@ export type ButtonType =
| 'secondary-light'
| 'default-light'
// TODO: Enforce that button always has a label
export function Button({
type = 'primary',
label,
@ -131,7 +132,8 @@ export function Button({
<Pressable
style={[typeOuterStyle, styles.outer, style]}
onPress={onPressWrapped}
testID={testID}>
testID={testID}
accessibilityRole="button">
{label ? (
<Text type="button" style={[typeLabelStyle, labelStyle]}>
{label}

View File

@ -1,4 +1,4 @@
import React, {useRef} from 'react'
import React, {PropsWithChildren, useMemo, useRef} from 'react'
import {
Dimensions,
StyleProp,
@ -39,6 +39,19 @@ type MaybeDropdownItem = DropdownItem | false | undefined
export type DropdownButtonType = ButtonType | 'bare'
interface DropdownButtonProps {
testID?: string
type?: DropdownButtonType
style?: StyleProp<ViewStyle>
items: MaybeDropdownItem[]
label?: string
menuWidth?: number
children?: React.ReactNode
openToRight?: boolean
rightOffset?: number
bottomOffset?: number
}
export function DropdownButton({
testID,
type = 'bare',
@ -50,18 +63,7 @@ export function DropdownButton({
openToRight = false,
rightOffset = 0,
bottomOffset = 0,
}: {
testID?: string
type?: DropdownButtonType
style?: StyleProp<ViewStyle>
items: MaybeDropdownItem[]
label?: string
menuWidth?: number
children?: React.ReactNode
openToRight?: boolean
rightOffset?: number
bottomOffset?: number
}) {
}: PropsWithChildren<DropdownButtonProps>) {
const ref1 = useRef<TouchableOpacity>(null)
const ref2 = useRef<View>(null)
@ -105,6 +107,18 @@ export function DropdownButton({
)
}
const numItems = useMemo(
() =>
items.filter(item => {
if (item === undefined || item === false) {
return false
}
return isBtn(item)
}).length,
[items],
)
if (type === 'bare') {
return (
<TouchableOpacity
@ -112,7 +126,10 @@ export function DropdownButton({
style={style}
onPress={onPress}
hitSlop={HITSLOP}
ref={ref1}>
ref={ref1}
accessibilityRole="button"
accessibilityLabel={`Opens ${numItems} options`}
accessibilityHint={`Opens ${numItems} options`}>
{children}
</TouchableOpacity>
)
@ -283,9 +300,20 @@ const DropdownItems = ({
const separatorColor =
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
const numItems = items.filter(isBtn).length
return (
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<TouchableWithoutFeedback
onPress={onOuterPress}
// TODO: Refactor dropdown components to:
// - (On web, if not handled by React Native) use semantic <select />
// and <option /> elements for keyboard navigation out of the box
// - (On mobile) be buttons by default, accept `label` and `nativeID`
// props, and always have an explicit label
accessibilityRole="button"
accessibilityLabel="Toggle dropdown"
accessibilityHint="">
<View style={[styles.bg]} />
</TouchableWithoutFeedback>
<View
@ -301,7 +329,9 @@ const DropdownItems = ({
testID={item.testID}
key={index}
style={[styles.menuItem]}
onPress={() => onPressItem(index)}>
onPress={() => onPressItem(index)}
accessibilityLabel={item.label}
accessibilityHint={`Option ${index + 1} of ${numItems}`}>
{item.icon && (
<FontAwesomeIcon
style={styles.icon}

View File

@ -62,12 +62,17 @@ export function AutoSizedImage({
onLongPress={onLongPress}
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
style={[styles.container, style]}>
style={[styles.container, style]}
accessible={true}
accessibilityLabel="Share image"
accessibilityHint="Opens ways of sharing image">
<Image
style={[styles.image, {aspectRatio}]}
source={uri}
accessible={true} // Must set for `accessibilityLabel` to work
accessibilityIgnoresInvertColors
accessibilityLabel={alt}
accessibilityHint=""
/>
{children}
</TouchableOpacity>
@ -80,7 +85,9 @@ export function AutoSizedImage({
style={[styles.image, {aspectRatio}]}
source={{uri}}
accessible={true} // Must set for `accessibilityLabel` to work
accessibilityIgnoresInvertColors
accessibilityLabel={alt}
accessibilityHint=""
/>
{children}
</View>

View File

@ -41,16 +41,25 @@ export const GalleryItem: FC<GalleryItemProps> = ({
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(index)}
onPressIn={() => onPressIn?.(index)}
onLongPress={() => onLongPress?.(index)}>
onLongPress={() => onLongPress?.(index)}
accessibilityRole="button"
accessibilityLabel="View image"
accessibilityHint="">
<Image
source={{uri: image.thumb}}
style={imageStyle}
accessible={true}
accessibilityLabel={image.alt}
accessibilityHint=""
accessibilityIgnoresInvertColors
/>
</TouchableOpacity>
{image.alt === '' ? null : (
<Pressable onPress={onPressAltText}>
<Pressable
onPress={onPressAltText}
accessibilityRole="button"
accessibilityLabel="View alt text"
accessibilityHint="Opens modal with alt text">
<Text style={styles.alt}>ALT</Text>
</Pressable>
)}

View File

@ -8,5 +8,7 @@ export function HighPriorityImage({source, ...props}: HighPriorityImageProps) {
const updatedSource = {
uri: typeof source === 'object' && source ? source.uri : '',
} satisfies ImageSource
return <Image source={updatedSource} {...props} />
return (
<Image accessibilityIgnoresInvertColors source={updatedSource} {...props} />
)
}

View File

@ -16,15 +16,33 @@ interface Props {
}
export function ImageHorzList({images, onPress, style}: Props) {
const numImages = images.length
return (
<View style={[styles.flexRow, style]}>
{images.map(({thumb, alt}, i) => (
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
<TouchableWithoutFeedback
key={i}
onPress={() => onPress?.(i)}
accessible={true}
accessibilityLabel={`Open image ${i} of ${numImages}`}
accessibilityHint="Opens image in viewer"
accessibilityActions={[{name: 'press', label: 'Press'}]}
onAccessibilityAction={action => {
switch (action.nativeEvent.actionName) {
case 'press':
onPress?.(0)
break
default:
break
}
}}>
<Image
source={{uri: thumb}}
style={styles.image}
accessible={true}
accessibilityLabel={alt}
accessibilityIgnoresInvertColors
accessibilityHint={alt}
accessibilityLabel=""
/>
</TouchableWithoutFeedback>
))}

View File

@ -23,7 +23,10 @@ export const LoadLatestBtn = ({
<TouchableOpacity
style={[pal.view, pal.borderDark, styles.loadLatest]}
onPress={onPress}
hitSlop={HITSLOP}>
hitSlop={HITSLOP}
accessibilityRole="button"
accessibilityLabel={`Load new ${label}`}
accessibilityHint="">
<Text type="md-bold" style={pal.text}>
<UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} />
Load new {label}

View File

@ -23,7 +23,10 @@ export const LoadLatestBtn = observer(
},
]}
onPress={onPress}
hitSlop={HITSLOP}>
hitSlop={HITSLOP}
accessibilityRole="button"
accessibilityLabel={`Load new ${label}`}
accessibilityHint={`Loads new ${label}`}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View File

@ -55,7 +55,14 @@ export function ContentHider({
</Text>
<TouchableOpacity
style={styles.showBtn}
onPress={() => setOverride(v => !v)}>
onPress={() => setOverride(v => !v)}
accessibilityLabel={override ? 'Hide post' : 'Show post'}
// TODO: The text labelling should be split up so controls have unique roles
accessibilityHint={
override
? 'Re-hide post'
: 'Shows post hidden based on your moderation settings'
}>
<Text type="md" style={pal.link}>
{override ? 'Hide' : 'Show'}
</Text>

View File

@ -46,7 +46,8 @@ export function PostHider({
</Text>
<TouchableOpacity
style={styles.showBtn}
onPress={() => setOverride(v => !v)}>
onPress={() => setOverride(v => !v)}
accessibilityRole="button">
<Text type="md" style={pal.link}>
{override ? 'Hide' : 'Show'} post
</Text>

View File

@ -136,7 +136,10 @@ export function PostEmbeds({
<Pressable
onPress={() => {
onPressAltText(alt)
}}>
}}
accessibilityRole="button"
accessibilityLabel="View alt text"
accessibilityHint="Opens modal with alt text">
<Text style={styles.alt}>ALT</Text>
</Pressable>
)}

View File

@ -184,7 +184,10 @@ function AppPassword({
<TouchableOpacity
testID={testID}
style={[styles.item, pal.border]}
onPress={onDelete}>
onPress={onDelete}
accessibilityRole="button"
accessibilityLabel="Delete"
accessibilityHint="Deletes app password">
<Text type="md-bold" style={pal.text}>
{name}
</Text>
@ -250,7 +253,6 @@ const styles = StyleSheet.create({
pr10: {
marginRight: 10,
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',

View File

@ -226,6 +226,9 @@ const FeedPage = observer(
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel="Compose"
accessibilityHint="Opens post composer"
/>
</View>
)

View File

@ -46,7 +46,9 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
<View key={`entry-${entry.id}`}>
<TouchableOpacity
style={[styles.entry, pal.border, pal.view]}
onPress={toggler(entry.id)}>
onPress={toggler(entry.id)}
accessibilityLabel="View debug entry"
accessibilityHint="Opens additional details for a debug entry">
{entry.type === 'debug' ? (
<FontAwesomeIcon icon="info" />
) : (

View File

@ -118,10 +118,10 @@ export const SearchScreen = withAuthRequired(
}, [])
return (
<TouchableWithoutFeedback onPress={onPress}>
<TouchableWithoutFeedback onPress={onPress} accessible={false}>
<View style={[pal.view, styles.container]}>
<HeaderWithInput
isInputFocused={isInputFocused}
isInputFocused={true}
query={query}
setIsInputFocused={setIsInputFocused}
onChangeQuery={onChangeQuery}

View File

@ -161,7 +161,9 @@ export const SettingsScreen = withAuthRequired(
<Link
href={`/profile/${store.me.handle}`}
title="Your profile"
noFeedback>
noFeedback
accessibilityLabel={`Signed in as ${store.me.handle}`}
accessibilityHint="Double tap to sign out">
<View style={[pal.view, styles.linkCard]}>
<View style={styles.avi}>
<UserAvatar size={40} avatar={store.me.avatar} />
@ -176,7 +178,10 @@ export const SettingsScreen = withAuthRequired(
</View>
<TouchableOpacity
testID="signOutBtn"
onPress={isSwitching ? undefined : onPressSignout}>
onPress={isSwitching ? undefined : onPressSignout}
accessibilityRole="button"
accessibilityLabel="Sign out"
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
<Text type="lg" style={pal.link}>
Sign out
</Text>
@ -191,7 +196,10 @@ export const SettingsScreen = withAuthRequired(
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
onPress={
isSwitching ? undefined : () => onPressSwitchAccount(account)
}>
}
accessibilityRole="button"
accessibilityLabel={`Switch to ${account.handle}`}
accessibilityHint="Switches the account you are logged in to">
<View style={styles.avi}>
<UserAvatar size={40} avatar={account.aviUrl} />
</View>
@ -209,7 +217,10 @@ export const SettingsScreen = withAuthRequired(
<TouchableOpacity
testID="switchToNewAccountBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressAddAccount}>
onPress={isSwitching ? undefined : onPressAddAccount}
accessibilityRole="button"
accessibilityLabel="Add account"
accessibilityHint="Create a new Bluesky account">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="plus"
@ -229,7 +240,10 @@ export const SettingsScreen = withAuthRequired(
<TouchableOpacity
testID="inviteFriendBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressInviteCodes}>
onPress={isSwitching ? undefined : onPressInviteCodes}
accessibilityRole="button"
accessibilityLabel="Invite"
accessibilityHint="Opens invite code list">
<View
style={[
styles.iconContainer,
@ -260,7 +274,9 @@ export const SettingsScreen = withAuthRequired(
<TouchableOpacity
testID="contentFilteringBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressContentFiltering}>
onPress={isSwitching ? undefined : onPressContentFiltering}
accessibilityHint="Content moderation"
accessibilityLabel="Opens configurable content moderation settings">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="eye"
@ -308,7 +324,10 @@ export const SettingsScreen = withAuthRequired(
<TouchableOpacity
testID="changeHandleBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressChangeHandle}>
onPress={isSwitching ? undefined : onPressChangeHandle}
accessibilityRole="button"
accessibilityLabel="Change handle"
accessibilityHint="Choose a new Bluesky username or create">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="at"
@ -327,7 +346,11 @@ export const SettingsScreen = withAuthRequired(
</Text>
<TouchableOpacity
style={[pal.view, styles.linkCard]}
onPress={onPressDeleteAccount}>
onPress={onPressDeleteAccount}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Delete account"
accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
<View style={[styles.iconContainer, dangerBg]}>
<FontAwesomeIcon
icon={['far', 'trash-can']}

View File

@ -56,7 +56,10 @@ export const Composer = observer(
}
return (
<Animated.View style={[styles.wrapper, pal.view, wrapperAnimStyle]}>
<Animated.View
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
aria-modal
accessibilityViewIsModal>
<ComposePost
replyTo={replyTo}
onPost={onPost}

View File

@ -31,7 +31,7 @@ export const Composer = observer(
}
return (
<View style={styles.mask}>
<View style={styles.mask} aria-modal accessibilityViewIsModal>
<View style={[styles.container, pal.view, pal.border]}>
<ComposePost
replyTo={replyTo}

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {
Linking,
SafeAreaView,
@ -50,6 +50,8 @@ export const DrawerContent = observer(() => {
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
const {notifications} = store.me
// events
// =
@ -120,7 +122,11 @@ export const DrawerContent = observer(() => {
]}>
<SafeAreaView style={s.flex1}>
<View style={styles.main}>
<TouchableOpacity testID="profileCardButton" onPress={onPressProfile}>
<TouchableOpacity
testID="profileCardButton"
accessibilityLabel="Profile"
accessibilityHint="Navigates to your profile"
onPress={onPressProfile}>
<UserAvatar size={80} avatar={store.me.avatar} />
<Text
type="title-lg"
@ -164,6 +170,8 @@ export const DrawerContent = observer(() => {
)
}
label="Search"
accessibilityLabel="Search"
accessibilityHint="Search through users and posts"
bold={isAtSearch}
onPress={onPressSearch}
/>
@ -184,6 +192,8 @@ export const DrawerContent = observer(() => {
)
}
label="Home"
accessibilityLabel="Home"
accessibilityHint="Navigates to default feed"
bold={isAtHome}
onPress={onPressHome}
/>
@ -204,7 +214,13 @@ export const DrawerContent = observer(() => {
)
}
label="Notifications"
count={store.me.notifications.unreadCountLabel}
accessibilityLabel={
notifications.unreadCountLabel === '1'
? 'Notifications: 1 unread notification'
: `Notifications: ${notifications.unreadCountLabel} unread notifications`
}
accessibilityHint="Opens notification feed"
count={notifications.unreadCountLabel}
bold={isAtNotifications}
onPress={onPressNotifications}
/>
@ -225,6 +241,8 @@ export const DrawerContent = observer(() => {
)
}
label="Profile"
accessibilityLabel="Profile"
accessibilityHint="See profile display name, avatar, description, and other profile items"
onPress={onPressProfile}
/>
<MenuItem
@ -236,6 +254,8 @@ export const DrawerContent = observer(() => {
/>
}
label="Settings"
accessibilityLabel="Settings"
accessibilityHint="Manage settings for your account, like handle, content moderation, and app passwords"
onPress={onPressSettings}
/>
</View>
@ -243,6 +263,13 @@ export const DrawerContent = observer(() => {
<View style={styles.footer}>
{!isWeb && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Toggle dark mode"
accessibilityHint={
theme.colorScheme === 'dark'
? 'Sets display to light mode'
: 'Sets display to dark mode'
}
onPress={onDarkmodePress}
style={[
styles.footerBtn,
@ -258,6 +285,9 @@ export const DrawerContent = observer(() => {
</TouchableOpacity>
)}
<TouchableOpacity
accessibilityRole="link"
accessibilityLabel="Send feedback"
accessibilityHint="Opens Google Forms feedback link"
onPress={onPressFeedback}
style={[
styles.footerBtn,
@ -281,25 +311,30 @@ export const DrawerContent = observer(() => {
)
})
function MenuItem({
icon,
label,
count,
bold,
onPress,
}: {
interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> {
icon: JSX.Element
label: string
count?: string
bold?: boolean
onPress: () => void
}) {
}
function MenuItem({
icon,
label,
accessibilityLabel,
count,
bold,
onPress,
}: MenuItemProps) {
const pal = usePalette('default')
return (
<TouchableOpacity
testID={`menuItemButton-${label}`}
style={styles.menuItem}
onPress={onPress}>
onPress={onPress}
accessibilityRole="menuitem"
accessibilityLabel={accessibilityLabel}
accessibilityHint="">
<View style={[styles.menuItemIconWrapper]}>
{icon}
{count ? (
@ -332,6 +367,7 @@ const InviteCodes = observer(() => {
const {track} = useAnalytics()
const store = useStores()
const pal = usePalette('default')
const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
track('Menu:ItemClicked', {url: '#invite-codes'})
store.shell.closeDrawer()
@ -341,7 +377,14 @@ const InviteCodes = observer(() => {
<TouchableOpacity
testID="menuItemInviteCodes"
style={[styles.inviteCodes]}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={
invitesAvailable === 1
? 'Invite codes: 1 available'
: `Invite codes: ${invitesAvailable} available`
}
accessibilityHint="Opens list of invite codes">
<FontAwesomeIcon
icon="ticket"
style={[

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {
Animated,
GestureResponderEvent,
@ -94,6 +94,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressHome}
accessibilityLabel="Go home"
accessibilityHint="Navigates to feed home"
/>
<Btn
testID="bottomBarSearchBtn"
@ -113,6 +115,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressSearch}
accessibilityRole="search"
/>
<Btn
testID="bottomBarNotificationsBtn"
@ -133,6 +136,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
}
onPress={onPressNotifications}
notificationCount={store.me.notifications.unreadCountLabel}
accessibilityLabel="Notifications"
accessibilityHint="Navigates to notifications"
/>
<Btn
testID="bottomBarProfileBtn"
@ -154,31 +159,43 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
</View>
}
onPress={onPressProfile}
accessibilityLabel="Profile"
accessibilityHint="Navigates to profile"
/>
</Animated.View>
)
})
interface BtnProps
extends Pick<
ComponentProps<typeof TouchableOpacity>,
'accessibilityRole' | 'accessibilityHint' | 'accessibilityLabel'
> {
testID?: string
icon: JSX.Element
notificationCount?: string
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}
function Btn({
testID,
icon,
notificationCount,
onPress,
onLongPress,
}: {
testID?: string
icon: JSX.Element
notificationCount?: string
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}) {
accessibilityHint,
accessibilityLabel,
}: BtnProps) {
return (
<TouchableOpacity
testID={testID}
style={styles.ctrl}
onPress={onLongPress ? onPress : undefined}
onPressIn={onLongPress ? undefined : onPress}
onLongPress={onLongPress}>
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}>
{notificationCount ? (
<View style={[styles.notificationCount]}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>

View File

@ -2,7 +2,11 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {useNavigation, useNavigationState} from '@react-navigation/native'
import {
useLinkProps,
useNavigation,
useNavigationState,
} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -59,7 +63,10 @@ function BackBtn() {
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={onPressBack}
style={styles.backBtn}>
style={styles.backBtn}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<FontAwesomeIcon
size={24}
icon="angle-left"
@ -86,25 +93,28 @@ const NavItem = observer(
}
return getCurrentRoute(state).name
})
const isCurrent = isTab(currentRouteName, pathName)
const {onPress} = useLinkProps({to: href})
return (
<PressableWithHover
style={styles.navItemWrapper}
hoverStyle={pal.viewLight}>
<Link href={href} style={styles.navItem}>
<View style={[styles.navItemIconWrapper]}>
{isCurrent ? iconFilled : icon}
{typeof count === 'string' && count ? (
<Text type="button" style={styles.navItemCount}>
{count}
</Text>
) : null}
</View>
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
{label}
</Text>
</Link>
hoverStyle={pal.viewLight}
onPress={onPress}
accessibilityLabel={label}
accessibilityHint={`Navigates to ${label}`}>
<View style={[styles.navItemIconWrapper]}>
{isCurrent ? iconFilled : icon}
{typeof count === 'string' && count ? (
<Text type="button" style={styles.navItemCount}>
{count}
</Text>
) : null}
</View>
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
{label}
</Text>
</PressableWithHover>
)
},
@ -115,7 +125,12 @@ function ComposeBtn() {
const onPressCompose = () => store.shell.openComposer({})
return (
<TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
<TouchableOpacity
style={[styles.newPostBtn]}
onPress={onPressCompose}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityHint="Opens post composer">
<View style={styles.newPostBtnIconWrapper}>
<ComposeIcon2
size={19}
@ -202,7 +217,7 @@ const styles = StyleSheet.create({
profileCard: {
marginVertical: 10,
width: 60,
width: 90,
paddingLeft: 12,
},
@ -215,21 +230,18 @@ const styles = StyleSheet.create({
},
navItemWrapper: {
paddingHorizontal: 12,
borderRadius: 8,
},
navItem: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 12,
paddingHorizontal: 12,
padding: 12,
borderRadius: 8,
gap: 10,
},
navItemIconWrapper: {
alignItems: 'center',
justifyContent: 'center',
width: 28,
height: 28,
marginRight: 10,
marginTop: 2,
},
navItemCount: {

View File

@ -61,7 +61,14 @@ export const DesktopRightNav = observer(function DesktopRightNav() {
<View>
<TouchableOpacity
style={[styles.darkModeToggle]}
onPress={onDarkmodePress}>
onPress={onDarkmodePress}
accessibilityRole="button"
accessibilityLabel="Toggle dark mode"
accessibilityHint={
mode === 'Dark'
? 'Sets display to light mode'
: 'Sets display to dark mode'
}>
<View style={[pal.viewLight, styles.darkModeToggleIcon]}>
<MoonIcon size={18} style={pal.textLight} />
</View>
@ -78,13 +85,22 @@ const InviteCodes = observer(() => {
const store = useStores()
const pal = usePalette('default')
const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
store.shell.openModal({name: 'invite-codes'})
}, [store])
return (
<TouchableOpacity
style={[styles.inviteCodes, 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">
<FontAwesomeIcon
icon="ticket"
style={[

View File

@ -67,10 +67,16 @@ export const DesktopSearch = observer(function DesktopSearch() {
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmit}
accessibilityRole="search"
/>
{query ? (
<View style={styles.cancelBtn}>
<TouchableOpacity onPress={onPressCancelSearch}>
<TouchableOpacity
onPress={onPressCancelSearch}
accessibilityRole="button"
accessibilityLabel="Cancel search"
accessibilityHint="Exits inputting search query"
onAccessibilityEscape={onPressCancelSearch}>
<Text type="lg" style={[pal.link]}>
Cancel
</Text>

View File

@ -46,7 +46,9 @@ const ShellInner = observer(() => {
{!isDesktop && store.shell.isDrawerOpen && (
<TouchableOpacity
onPress={() => store.shell.closeDrawer()}
style={styles.drawerMask}>
style={styles.drawerMask}
accessibilityLabel="Close navigation footer"
accessibilityHint="Closes bottom navigation bar">
<View style={styles.drawerContainer}>
<DrawerContent />
</View>

View File

@ -60,10 +60,6 @@
}
}*/
/* Remove focus state on inputs */
*:focus {
outline: 0;
}
/* Remove default link styling */
a {
color: inherit;
@ -102,6 +98,14 @@
color: #0085ff;
cursor: pointer;
}
/* OLLIE: TODO -- this is not accessible */
/* Remove focus state on inputs */
.ProseMirror-focused {
outline: 0;
}
input:focus {
outline: 0;
}
.tippy-content .items {
border-radius: 6px;
background: #F3F3F8;

2322
yarn.lock

File diff suppressed because it is too large Load Diff