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