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 = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: '@react-native-community',
|
extends: ['@react-native-community', 'plugin:react-native-a11y/ios'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['@typescript-eslint', 'detox'],
|
plugins: ['@typescript-eslint', 'detox'],
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
|
|
|
@ -57,8 +57,9 @@
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
/* OLLIE: TODO -- this is not accessible */
|
||||||
/* Remove focus state on inputs */
|
/* Remove focus state on inputs */
|
||||||
*:focus {
|
input:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
/* Remove default link styling */
|
/* Remove default link styling */
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"await-lock": "^2.2.2",
|
"await-lock": "^2.2.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
|
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||||
"expo": "~48.0.15",
|
"expo": "~48.0.15",
|
||||||
"expo-application": "~5.1.1",
|
"expo-application": "~5.1.1",
|
||||||
"expo-build-properties": "~0.5.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 {
|
export function sanitizeDisplayName(str: string): string {
|
||||||
if (typeof str === 'string') {
|
if (typeof str === 'string') {
|
||||||
return str.replace(CHECK_MARKS_RE, '')
|
return str.replace(CHECK_MARKS_RE, '').trim()
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ export const s = StyleSheet.create({
|
||||||
mr2: {marginRight: 2},
|
mr2: {marginRight: 2},
|
||||||
mr5: {marginRight: 5},
|
mr5: {marginRight: 5},
|
||||||
mr10: {marginRight: 10},
|
mr10: {marginRight: 10},
|
||||||
|
mr20: {marginRight: 20},
|
||||||
ml2: {marginLeft: 2},
|
ml2: {marginLeft: 2},
|
||||||
ml5: {marginLeft: 5},
|
ml5: {marginLeft: 5},
|
||||||
ml10: {marginLeft: 10},
|
ml10: {marginLeft: 10},
|
||||||
|
@ -149,6 +150,7 @@ export const s = StyleSheet.create({
|
||||||
pb5: {paddingBottom: 5},
|
pb5: {paddingBottom: 5},
|
||||||
pb10: {paddingBottom: 10},
|
pb10: {paddingBottom: 10},
|
||||||
pb20: {paddingBottom: 20},
|
pb20: {paddingBottom: 20},
|
||||||
|
px5: {paddingHorizontal: 5},
|
||||||
|
|
||||||
// flex
|
// flex
|
||||||
flexRow: {flexDirection: 'row'},
|
flexRow: {flexDirection: 'row'},
|
||||||
|
|
|
@ -28,7 +28,10 @@ export const SplashScreen = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="createAccountButton"
|
testID="createAccountButton"
|
||||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
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]}>
|
<Text style={[s.white, styles.btnLabel]}>
|
||||||
Create a new account
|
Create a new account
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -36,7 +39,10 @@ export const SplashScreen = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="signInButton"
|
testID="signInButton"
|
||||||
style={[styles.btn, pal.btn]}
|
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>
|
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -43,7 +43,9 @@ export const SplashScreen = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="createAccountButton"
|
testID="createAccountButton"
|
||||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||||
onPress={onPressCreateAccount}>
|
onPress={onPressCreateAccount}
|
||||||
|
// TODO: web accessibility
|
||||||
|
accessibilityRole="button">
|
||||||
<Text style={[s.white, styles.btnLabel]}>
|
<Text style={[s.white, styles.btnLabel]}>
|
||||||
Create a new account
|
Create a new account
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -51,7 +53,9 @@ export const SplashScreen = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="signInButton"
|
testID="signInButton"
|
||||||
style={[styles.btn, pal.btn]}
|
style={[styles.btn, pal.btn]}
|
||||||
onPress={onPressSignin}>
|
onPress={onPressSignin}
|
||||||
|
// TODO: web accessibility
|
||||||
|
accessibilityRole="button">
|
||||||
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -60,7 +64,10 @@ export const SplashScreen = ({
|
||||||
style={[styles.notice, pal.textLight]}
|
style={[styles.notice, pal.textLight]}
|
||||||
lineHeight={1.3}>
|
lineHeight={1.3}>
|
||||||
Bluesky will launch soon.{' '}
|
Bluesky will launch soon.{' '}
|
||||||
<TouchableOpacity onPress={onPressWaitlist}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressWaitlist}
|
||||||
|
// TODO: web accessibility
|
||||||
|
accessibilityRole="button">
|
||||||
<Text type="xl" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
Join the waitlist
|
Join the waitlist
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -72,14 +72,24 @@ export const CreateAccount = observer(
|
||||||
{model.step === 3 && <Step3 model={model} />}
|
{model.step === 3 && <Step3 model={model} />}
|
||||||
</View>
|
</View>
|
||||||
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
<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}>
|
<Text type="xl" style={pal.link}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
{model.canNext ? (
|
{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 ? (
|
{model.isProcessing ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
|
@ -91,7 +101,11 @@ export const CreateAccount = observer(
|
||||||
) : model.didServiceDescriptionFetchFail ? (
|
) : model.didServiceDescriptionFetchFail ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="retryConnectBtn"
|
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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
Retry
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
<View>
|
<View>
|
||||||
<StepHeader step="1" title="Your hosting provider" />
|
<StepHeader step="1" title="Your hosting provider" />
|
||||||
<Text style={[pal.text, s.mb10]}>
|
<Text style={[pal.text, s.mb10]}>
|
||||||
This is the company that keeps you online.
|
This is the service that keeps you online.
|
||||||
</Text>
|
</Text>
|
||||||
<Option
|
<Option
|
||||||
testID="blueskyServerBtn"
|
testID="blueskyServerBtn"
|
||||||
|
@ -72,7 +72,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
label="Other"
|
label="Other"
|
||||||
onPress={onPressOther}>
|
onPress={onPressOther}>
|
||||||
<View style={styles.otherForm}>
|
<View style={styles.otherForm}>
|
||||||
<Text style={[pal.text, s.mb5]}>
|
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
|
||||||
Enter the address of your provider:
|
Enter the address of your provider:
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -82,6 +82,9 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
value={model.serviceUrl}
|
value={model.serviceUrl}
|
||||||
editable
|
editable
|
||||||
onChange={onChangeServiceUrl}
|
onChange={onChangeServiceUrl}
|
||||||
|
accessibilityHint="Input hosting provider address"
|
||||||
|
accessibilityLabel="Hosting provider address"
|
||||||
|
accessibilityLabelledBy="addressProvider"
|
||||||
/>
|
/>
|
||||||
{LOGIN_INCLUDE_DEV_SERVERS && (
|
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||||
<View style={[s.flexRow, s.mt10]}>
|
<View style={[s.flexRow, s.mt10]}>
|
||||||
|
@ -136,7 +139,12 @@ function Option({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.option, pal.border]}>
|
<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.optionHeading}>
|
||||||
<View style={[styles.circle, pal.border]}>
|
<View style={[styles.circle, pal.border]}>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
|
|
|
@ -41,6 +41,9 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
value={model.inviteCode}
|
value={model.inviteCode}
|
||||||
editable
|
editable
|
||||||
onChange={model.setInviteCode}
|
onChange={model.setInviteCode}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Invite code"
|
||||||
|
accessibilityHint="Input invite code to proceed"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -48,7 +51,11 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
{!model.inviteCode && model.isInviteCodeRequired ? (
|
{!model.inviteCode && model.isInviteCodeRequired ? (
|
||||||
<Text style={[s.alignBaseline, pal.text]}>
|
<Text style={[s.alignBaseline, pal.text]}>
|
||||||
Don't have an invite code?{' '}
|
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>
|
<Text style={pal.link}>Join the waitlist</Text>
|
||||||
</TouchableWithoutFeedback>{' '}
|
</TouchableWithoutFeedback>{' '}
|
||||||
to try the beta before it's publicly available.
|
to try the beta before it's publicly available.
|
||||||
|
@ -56,7 +63,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<View style={s.pb20}>
|
<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
|
Email address
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -66,11 +73,17 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
value={model.email}
|
value={model.email}
|
||||||
editable
|
editable
|
||||||
onChange={model.setEmail}
|
onChange={model.setEmail}
|
||||||
|
accessibilityLabel="Email"
|
||||||
|
accessibilityHint="Input email for Bluesky waitlist"
|
||||||
|
accessibilityLabelledBy="email"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={s.pb20}>
|
<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
|
Password
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -81,17 +94,27 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
editable
|
editable
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
onChange={model.setPassword}
|
onChange={model.setPassword}
|
||||||
|
accessibilityLabel="Password"
|
||||||
|
accessibilityHint="Set password"
|
||||||
|
accessibilityLabelledBy="password"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={s.pb20}>
|
<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
|
Legal check
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="is13Input"
|
testID="is13Input"
|
||||||
style={[styles.toggleBtn, pal.border]}
|
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]}>
|
<View style={[pal.borderDark, styles.checkbox]}>
|
||||||
{model.is13 && (
|
{model.is13 && (
|
||||||
<FontAwesomeIcon icon="check" style={s.blue3} size={16} />
|
<FontAwesomeIcon icon="check" style={s.blue3} size={16} />
|
||||||
|
|
|
@ -23,6 +23,9 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
value={model.handle}
|
value={model.handle}
|
||||||
editable
|
editable
|
||||||
onChange={model.setHandle}
|
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]}>
|
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||||
Your full handle will be{' '}
|
Your full handle will be{' '}
|
||||||
|
|
|
@ -195,7 +195,10 @@ const ChooseAccountForm = ({
|
||||||
testID={`chooseAccountBtn-${account.handle}`}
|
testID={`chooseAccountBtn-${account.handle}`}
|
||||||
key={account.did}
|
key={account.did}
|
||||||
style={[pal.view, pal.border, styles.account]}
|
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
|
<View
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
<View style={s.p10}>
|
<View style={s.p10}>
|
||||||
|
@ -220,7 +223,10 @@ const ChooseAccountForm = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="chooseNewAccountBtn"
|
testID="chooseNewAccountBtn"
|
||||||
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
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]}>
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||||
<Text type="lg" style={pal.text}>
|
<Text type="lg" style={pal.text}>
|
||||||
|
@ -235,7 +241,11 @@ const ChooseAccountForm = ({
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
<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]}>
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -351,7 +361,10 @@ const LoginForm = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="loginSelectServiceButton"
|
testID="loginSelectServiceButton"
|
||||||
style={styles.textBtn}
|
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]}>
|
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||||
{toNiceDomain(serviceUrl)}
|
{toNiceDomain(serviceUrl)}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -386,6 +399,8 @@ const LoginForm = ({
|
||||||
value={identifier}
|
value={identifier}
|
||||||
onChangeText={str => setIdentifier((str || '').toLowerCase())}
|
onChangeText={str => setIdentifier((str || '').toLowerCase())}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel="Username or email address"
|
||||||
|
accessibilityHint="Input the username or email address you used at signup"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
@ -402,14 +417,28 @@ const LoginForm = ({
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
secureTextEntry
|
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}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel="Password"
|
||||||
|
accessibilityHint={
|
||||||
|
identifier === ''
|
||||||
|
? 'Input your password'
|
||||||
|
: `Input the password tied to ${identifier}`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="forgotPasswordButton"
|
testID="forgotPasswordButton"
|
||||||
style={styles.textInputInnerBtn}
|
style={styles.textInputInnerBtn}
|
||||||
onPress={onPressForgotPassword}>
|
onPress={onPressForgotPassword}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Forgot password"
|
||||||
|
accessibilityHint="Opens password reset form">
|
||||||
<Text style={pal.link}>Forgot</Text>
|
<Text style={pal.link}>Forgot</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -425,7 +454,11 @@ const LoginForm = ({
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
<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]}>
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -434,7 +467,10 @@ const LoginForm = ({
|
||||||
{!serviceDescription && error ? (
|
{!serviceDescription && error ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="loginRetryButton"
|
testID="loginRetryButton"
|
||||||
onPress={onPressRetryConnect}>
|
onPress={onPressRetryConnect}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Retry"
|
||||||
|
accessibilityHint="Retries login">
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
Retry
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -449,7 +485,12 @@ const LoginForm = ({
|
||||||
) : isProcessing ? (
|
) : isProcessing ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : isReady ? (
|
) : 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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Next
|
Next
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -539,7 +580,10 @@ const ForgotPasswordForm = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="forgotPasswordSelectServiceButton"
|
testID="forgotPasswordSelectServiceButton"
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||||
onPress={onPressSelectService}>
|
onPress={onPressSelectService}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Hosting provider"
|
||||||
|
accessibilityHint="Sets hosting provider for password reset">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="globe"
|
icon="globe"
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
@ -572,6 +616,8 @@ const ForgotPasswordForm = ({
|
||||||
value={email}
|
value={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel="Email"
|
||||||
|
accessibilityHint="Sets email for password reset"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -586,7 +632,11 @@ const ForgotPasswordForm = ({
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
<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]}>
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -599,7 +649,12 @@ const ForgotPasswordForm = ({
|
||||||
Next
|
Next
|
||||||
</Text>
|
</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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Next
|
Next
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -699,6 +754,9 @@ const SetNewPasswordForm = ({
|
||||||
value={resetCode}
|
value={resetCode}
|
||||||
onChangeText={setResetCode}
|
onChangeText={setResetCode}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Reset code"
|
||||||
|
accessibilityHint="Input code sent to your email for password reset"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
@ -718,6 +776,9 @@ const SetNewPasswordForm = ({
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Password"
|
||||||
|
accessibilityHint="Input new password"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -732,7 +793,11 @@ const SetNewPasswordForm = ({
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
<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]}>
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -747,7 +812,10 @@ const SetNewPasswordForm = ({
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="setNewPasswordButton"
|
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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Next
|
Next
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -783,7 +851,11 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
<View style={s.flex1} />
|
<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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Okay
|
Okay
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
import React from 'react'
|
import React, {ComponentProps} from 'react'
|
||||||
import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
|
import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
|
||||||
export function TextInput({
|
interface Props extends Omit<ComponentProps<typeof RNTextInput>, 'onChange'> {
|
||||||
testID,
|
|
||||||
icon,
|
|
||||||
value,
|
|
||||||
placeholder,
|
|
||||||
editable,
|
|
||||||
secureTextEntry,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
testID?: string
|
testID?: string
|
||||||
icon: IconProp
|
icon: IconProp
|
||||||
value: string
|
|
||||||
placeholder: string
|
|
||||||
editable: boolean
|
|
||||||
secureTextEntry?: boolean
|
|
||||||
onChange: (v: string) => void
|
onChange: (v: string) => void
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function TextInput({testID, icon, onChange, ...props}: Props) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
|
@ -30,15 +20,12 @@ export function TextInput({
|
||||||
<RNTextInput
|
<RNTextInput
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[pal.text, styles.textInput]}
|
style={[pal.text, styles.textInput]}
|
||||||
placeholder={placeholder}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
placeholderTextColor={pal.colors.textLight}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
secureTextEntry={secureTextEntry}
|
|
||||||
value={value}
|
|
||||||
onChangeText={v => onChange(v)}
|
onChangeText={v => onChange(v)}
|
||||||
editable={editable}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
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 {ExternalEmbed} from './ExternalEmbed'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import * as Toast from '../util/Toast'
|
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 {TextInput, TextInputRef} from './text-input/TextInput'
|
||||||
import {CharProgress} from './char-progress/CharProgress'
|
import {CharProgress} from './char-progress/CharProgress'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
@ -87,27 +88,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
}, [autocompleteView])
|
}, [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(
|
const onPressAddLinkCard = useCallback(
|
||||||
(uri: string) => {
|
(uri: string) => {
|
||||||
setExtLink({uri, isLoading: true})
|
setExtLink({uri, isLoading: true})
|
||||||
|
@ -133,7 +113,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
|
|
||||||
if (rt.text.trim().length === 0 && gallery.isEmpty) {
|
if (rt.text.trim().length === 0 && gallery.isEmpty) {
|
||||||
setError('Did you want to say anything?')
|
setError('Did you want to say anything?')
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
@ -203,133 +183,149 @@ export const ComposePost = observer(function ComposePost({
|
||||||
testID="composePostView"
|
testID="composePostView"
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
style={styles.outer}>
|
style={styles.outer}>
|
||||||
<TouchableWithoutFeedback onPressIn={onPressContainer}>
|
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
|
||||||
<View style={[s.flex1, viewStyles]}>
|
<View style={styles.topbar}>
|
||||||
<View style={styles.topbar}>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
testID="composerCancelButton"
|
||||||
testID="composerCancelButton"
|
onPress={hackfixOnClose}
|
||||||
onPress={hackfixOnClose}>
|
onAccessibilityEscape={hackfixOnClose}
|
||||||
<Text style={[pal.link, s.f18]}>Cancel</Text>
|
accessibilityRole="button"
|
||||||
</TouchableOpacity>
|
accessibilityLabel="Cancel"
|
||||||
<View style={s.flex1} />
|
accessibilityHint="Closes post composer">
|
||||||
{isProcessing ? (
|
<Text style={[pal.link, s.f18]}>Cancel</Text>
|
||||||
<View style={styles.postBtn}>
|
</TouchableOpacity>
|
||||||
<ActivityIndicator />
|
<View style={s.flex1} />
|
||||||
</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>
|
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<View style={[pal.btn, styles.processingLine]}>
|
<View style={styles.postBtn}>
|
||||||
<Text style={pal.text}>{processingState}</Text>
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : canPost ? (
|
||||||
{error !== '' && (
|
<TouchableOpacity
|
||||||
<View style={styles.errorLine}>
|
testID="composerPublishBtn"
|
||||||
<View style={styles.errorIcon}>
|
onPress={() => {
|
||||||
<FontAwesomeIcon
|
onPressPublish(richtext)
|
||||||
icon="exclamation"
|
}}
|
||||||
style={{color: colors.red4}}
|
accessibilityRole="button"
|
||||||
size={10}
|
accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'}
|
||||||
/>
|
accessibilityHint={
|
||||||
</View>
|
replyTo
|
||||||
<Text style={[s.red4, s.flex1]}>{error}</Text>
|
? '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>
|
</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>
|
</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>
|
</KeyboardAvoidingView>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,7 +60,13 @@ export const ExternalEmbed = ({
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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} />
|
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -13,7 +13,10 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="replyPromptBtn"
|
testID="replyPromptBtn"
|
||||||
style={[pal.view, pal.border, styles.prompt]}
|
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} />
|
<UserAvatar avatar={store.me.avatar} size={38} />
|
||||||
<Text
|
<Text
|
||||||
type="xl"
|
type="xl"
|
||||||
|
|
|
@ -107,6 +107,9 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
<View key={`selected-image-${image.path}`} style={[imageStyle]}>
|
<View key={`selected-image-${image.path}`} style={[imageStyle]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="altTextButton"
|
testID="altTextButton"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Add alt text"
|
||||||
|
accessibilityHint="Opens modal for inputting image alt text"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
handleAddImageAltText(image)
|
handleAddImageAltText(image)
|
||||||
}}
|
}}
|
||||||
|
@ -116,6 +119,9 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
<View style={imageControlsSubgroupStyle}>
|
<View style={imageControlsSubgroupStyle}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="cropPhotoButton"
|
testID="cropPhotoButton"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Crop image"
|
||||||
|
accessibilityHint="Opens modal for cropping image"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
handleEditPhoto(image)
|
handleEditPhoto(image)
|
||||||
}}
|
}}
|
||||||
|
@ -128,6 +134,9 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="removePhotoButton"
|
testID="removePhotoButton"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Remove image"
|
||||||
|
accessibilityHint=""
|
||||||
onPress={() => handleRemovePhoto(image)}
|
onPress={() => handleRemovePhoto(image)}
|
||||||
style={styles.imageControl}>
|
style={styles.imageControl}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -144,6 +153,8 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
source={{
|
source={{
|
||||||
uri: image.compressed.path,
|
uri: image.compressed.path,
|
||||||
}}
|
}}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : null,
|
) : null,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {TouchableOpacity} from 'react-native'
|
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
|
@ -7,7 +7,6 @@ import {
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
import {openCamera} from 'lib/media/picker'
|
import {openCamera} from 'lib/media/picker'
|
||||||
import {useCameraPermission} from 'lib/hooks/usePermissions'
|
import {useCameraPermission} from 'lib/hooks/usePermissions'
|
||||||
|
@ -54,8 +53,11 @@ export function OpenCameraBtn({gallery}: Props) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="openCameraButton"
|
testID="openCameraButton"
|
||||||
onPress={onPressTakePicture}
|
onPress={onPressTakePicture}
|
||||||
style={[s.pl5]}
|
style={styles.button}
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Camera"
|
||||||
|
accessibilityHint="Opens camera on device">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
style={pal.link as FontAwesomeIconStyle}
|
style={pal.link as FontAwesomeIconStyle}
|
||||||
|
@ -64,3 +66,9 @@ export function OpenCameraBtn({gallery}: Props) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {TouchableOpacity} from 'react-native'
|
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
import {GalleryModel} from 'state/models/media/gallery'
|
||||||
|
@ -36,8 +35,11 @@ export function SelectPhotoBtn({gallery}: Props) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="openGalleryBtn"
|
testID="openGalleryBtn"
|
||||||
onPress={onPressSelectPhotos}
|
onPress={onPressSelectPhotos}
|
||||||
style={[s.pl5, s.pr20]}
|
style={styles.button}
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Gallery"
|
||||||
|
accessibilityHint="Opens device photo gallery">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'image']}
|
icon={['far', 'image']}
|
||||||
style={pal.link as FontAwesomeIconStyle}
|
style={pal.link as FontAwesomeIconStyle}
|
||||||
|
@ -46,3 +48,9 @@ export function SelectPhotoBtn({gallery}: Props) {
|
||||||
</TouchableOpacity>
|
</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 {
|
import {
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
TextInput as RNTextInput,
|
||||||
TextInputSelectionChangeEventData,
|
TextInputSelectionChangeEventData,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
@ -27,14 +34,14 @@ export interface TextInputRef {
|
||||||
blur: () => void
|
blur: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextInputProps {
|
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
|
||||||
richtext: RichText
|
richtext: RichText
|
||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteModel
|
autocompleteView: UserAutocompleteModel
|
||||||
setRichText: (v: RichText) => void
|
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onPressPublish: (richtext: RichText) => Promise<false | undefined>
|
onPressPublish: (richtext: RichText) => Promise<void>
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
}
|
}
|
||||||
|
@ -55,6 +62,7 @@ export const TextInput = forwardRef(
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onSuggestedLinksChanged,
|
onSuggestedLinksChanged,
|
||||||
onError,
|
onError,
|
||||||
|
...props
|
||||||
}: TextInputProps,
|
}: TextInputProps,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
@ -65,26 +73,11 @@ export const TextInput = forwardRef(
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => textInput.current?.focus(),
|
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(
|
const onChangeText = useCallback(
|
||||||
async (newText: string) => {
|
async (newText: string) => {
|
||||||
const newRt = new RichText({text: newText})
|
const newRt = new RichText({text: newText})
|
||||||
|
@ -206,8 +199,10 @@ export const TextInput = forwardRef(
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
placeholderTextColor={pal.colors.textLight}
|
placeholderTextColor={pal.colors.textLight}
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
autoFocus={true}
|
||||||
multiline
|
multiline
|
||||||
style={[pal.text, styles.textInput, styles.textInputFormatting]}>
|
style={[pal.text, styles.textInput, styles.textInputFormatting]}
|
||||||
|
{...props}>
|
||||||
{textDecorated}
|
{textDecorated}
|
||||||
</PasteInput>
|
</PasteInput>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
|
|
@ -25,9 +25,9 @@ interface TextInputProps {
|
||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteModel
|
autocompleteView: UserAutocompleteModel
|
||||||
setRichText: (v: RichText) => void
|
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onPressPublish: (richtext: RichText) => Promise<false | undefined>
|
onPressPublish: (richtext: RichText) => Promise<void>
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,9 @@ export const Autocomplete = observer(
|
||||||
testID="autocompleteButton"
|
testID="autocompleteButton"
|
||||||
key={item.handle}
|
key={item.handle}
|
||||||
style={[pal.border, styles.item]}
|
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}>
|
<Text type="md-medium" style={pal.text}>
|
||||||
{item.displayName || item.handle}
|
{item.displayName || item.handle}
|
||||||
<Text type="sm" style={pal.textLight}>
|
<Text type="sm" style={pal.textLight}>
|
||||||
|
|
|
@ -20,7 +20,11 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.closeButton}
|
style={styles.closeButton}
|
||||||
onPress={onRequestClose}
|
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>
|
<Text style={styles.closeText}>✕</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
@ -127,7 +127,8 @@ const ImageItem = ({
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
|
onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
|
||||||
onLongPress={onLongPressHandler}
|
onLongPress={onLongPressHandler}
|
||||||
delayLongPress={delayLongPress}>
|
delayLongPress={delayLongPress}
|
||||||
|
accessibilityRole="image">
|
||||||
<Animated.Image
|
<Animated.Image
|
||||||
source={imageSrc}
|
source={imageSrc}
|
||||||
style={imageStylesWithOpacity}
|
style={imageStylesWithOpacity}
|
||||||
|
|
|
@ -112,7 +112,12 @@ function ImageViewing({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.screen} onLayout={onLayout} edges={edges}>
|
<SafeAreaView
|
||||||
|
style={styles.screen}
|
||||||
|
onLayout={onLayout}
|
||||||
|
edges={edges}
|
||||||
|
aria-modal
|
||||||
|
accessibilityViewIsModal>
|
||||||
<ModalsContainer />
|
<ModalsContainer />
|
||||||
<View style={[styles.container, {opacity, backgroundColor}]}>
|
<View style={[styles.container, {opacity, backgroundColor}]}>
|
||||||
<Animated.View style={[styles.header, {transform: headerTransform}]}>
|
<Animated.View style={[styles.header, {transform: headerTransform}]}>
|
||||||
|
|
|
@ -89,13 +89,25 @@ function LightboxInner({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.mask}>
|
<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}>
|
<View style={styles.imageCenterer}>
|
||||||
<Image source={imgs[index]} style={styles.image} />
|
<Image
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
source={imgs[index]}
|
||||||
|
style={styles.image}
|
||||||
|
/>
|
||||||
{canGoLeft && (
|
{canGoLeft && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressLeft}
|
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
|
<FontAwesomeIcon
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
|
@ -106,7 +118,10 @@ function LightboxInner({
|
||||||
{canGoRight && (
|
{canGoRight && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressRight}
|
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
|
<FontAwesomeIcon
|
||||||
icon="angle-right"
|
icon="angle-right"
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
|
|
|
@ -122,12 +122,18 @@ export function Component({}: {}) {
|
||||||
editable={!appPassword}
|
editable={!appPassword}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onEndEditing={createAppPassword}
|
onEndEditing={createAppPassword}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Name"
|
||||||
|
accessibilityHint="Input name for app password"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[pal.border, styles.passwordContainer, pal.btn]}
|
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]}>
|
<Text type="2xl-bold" style={[pal.text]}>
|
||||||
{appPassword}
|
{appPassword}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -37,7 +37,8 @@ export function Component({prevAltText, onAltTextSet}: Props) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
testID="altTextImageModal"
|
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>
|
<Text style={[styles.title, pal.text]}>Add alt text</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="altTextImageInput"
|
testID="altTextImageInput"
|
||||||
|
@ -46,9 +47,17 @@ export function Component({prevAltText, onAltTextSet}: Props) {
|
||||||
multiline
|
multiline
|
||||||
value={altText}
|
value={altText}
|
||||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
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}>
|
<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
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
@ -61,7 +70,11 @@ export function Component({prevAltText, onAltTextSet}: Props) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="altTextImageCancelBtn"
|
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]}>
|
<View style={[styles.button]}>
|
||||||
<Text type="button-lg" style={[pal.textLight]}>
|
<Text type="button-lg" style={[pal.textLight]}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -30,7 +30,12 @@ export function Component({altText}: Props) {
|
||||||
<View style={[styles.text, pal.viewLight]}>
|
<View style={[styles.text, pal.viewLight]}>
|
||||||
<Text style={pal.text}>{altText}</Text>
|
<Text style={pal.text}>{altText}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity testID="altTextImageSaveBtn" onPress={onPress}>
|
<TouchableOpacity
|
||||||
|
testID="altTextImageSaveBtn"
|
||||||
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Save"
|
||||||
|
accessibilityHint="Save alt text">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -133,7 +133,12 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
<View style={[s.flex1, pal.view]}>
|
<View style={[s.flex1, pal.view]}>
|
||||||
<View style={[styles.title, pal.border]}>
|
<View style={[styles.title, pal.border]}>
|
||||||
<View style={styles.titleLeft}>
|
<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}>
|
<Text type="lg" style={pal.textLight}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -148,13 +153,20 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
) : error && !serviceDescription ? (
|
) : error && !serviceDescription ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="retryConnectButton"
|
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]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
Retry
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : canSave ? (
|
) : 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}>
|
<Text type="2xl-medium" style={pal.link}>
|
||||||
Save
|
Save
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -245,6 +257,9 @@ function ProvidedHandleForm({
|
||||||
value={handle}
|
value={handle}
|
||||||
onChangeText={onChangeHandle}
|
onChangeText={onChangeHandle}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Handle"
|
||||||
|
accessibilityHint="Sets Bluesky username"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
|
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
|
||||||
|
@ -253,7 +268,11 @@ function ProvidedHandleForm({
|
||||||
@{createFullHandle(handle, userDomain)}
|
@{createFullHandle(handle, userDomain)}
|
||||||
</Text>
|
</Text>
|
||||||
</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]}>
|
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||||
I have my own domain
|
I have my own domain
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -338,7 +357,7 @@ function CustomHandleForm({
|
||||||
// =
|
// =
|
||||||
return (
|
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
|
Enter the domain you want to use
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[pal.btn, styles.textInputWrapper]}>
|
<View style={[pal.btn, styles.textInputWrapper]}>
|
||||||
|
@ -356,6 +375,9 @@ function CustomHandleForm({
|
||||||
value={handle}
|
value={handle}
|
||||||
onChangeText={onChangeHandle}
|
onChangeText={onChangeHandle}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
|
accessibilityLabelledBy="customDomain"
|
||||||
|
accessibilityLabel="Custom domain"
|
||||||
|
accessibilityHint="Input your preferred hosting provider"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.spacer} />
|
<View style={styles.spacer} />
|
||||||
|
@ -421,7 +443,10 @@ function CustomHandleForm({
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<View style={styles.spacer} />
|
<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]}>
|
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||||
Nevermind, create a handle for me
|
Nevermind, create a handle for me
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -66,7 +66,12 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="confirmBtn"
|
testID="confirmBtn"
|
||||||
onPress={onPress}
|
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>
|
<Text style={[s.white, s.bold, s.f18]}>Confirm</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -34,7 +34,12 @@ export function Component({}: {}) {
|
||||||
<View style={styles.bottomSpacer} />
|
<View style={styles.bottomSpacer} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={[styles.btnContainer, pal.borderDark]}>
|
<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
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
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(
|
const ContentLabelPref = observer(
|
||||||
({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
|
({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -67,19 +73,20 @@ const ContentLabelPref = observer(
|
||||||
<SelectGroup
|
<SelectGroup
|
||||||
current={store.preferences.contentLabels[group]}
|
current={store.preferences.contentLabels[group]}
|
||||||
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
||||||
|
group={group}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function SelectGroup({
|
interface SelectGroupProps {
|
||||||
current,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
current: LabelPreference
|
current: LabelPreference
|
||||||
onChange: (v: LabelPreference) => void
|
onChange: (v: LabelPreference) => void
|
||||||
}) {
|
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({current, onChange, group}: SelectGroupProps) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.selectableBtns}>
|
<View style={styles.selectableBtns}>
|
||||||
<SelectableBtn
|
<SelectableBtn
|
||||||
|
@ -88,12 +95,14 @@ function SelectGroup({
|
||||||
label="Hide"
|
label="Hide"
|
||||||
left
|
left
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
group={group}
|
||||||
/>
|
/>
|
||||||
<SelectableBtn
|
<SelectableBtn
|
||||||
current={current}
|
current={current}
|
||||||
value="warn"
|
value="warn"
|
||||||
label="Warn"
|
label="Warn"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
group={group}
|
||||||
/>
|
/>
|
||||||
<SelectableBtn
|
<SelectableBtn
|
||||||
current={current}
|
current={current}
|
||||||
|
@ -101,11 +110,22 @@ function SelectGroup({
|
||||||
label="Show"
|
label="Show"
|
||||||
right
|
right
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
group={group}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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({
|
function SelectableBtn({
|
||||||
current,
|
current,
|
||||||
value,
|
value,
|
||||||
|
@ -113,14 +133,8 @@ function SelectableBtn({
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
group,
|
||||||
current: string
|
}: SelectableBtnProps) {
|
||||||
value: LabelPreference
|
|
||||||
label: string
|
|
||||||
left?: boolean
|
|
||||||
right?: boolean
|
|
||||||
onChange: (v: LabelPreference) => void
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palPrimary = usePalette('inverted')
|
const palPrimary = usePalette('inverted')
|
||||||
return (
|
return (
|
||||||
|
@ -132,7 +146,10 @@ function SelectableBtn({
|
||||||
pal.border,
|
pal.border,
|
||||||
current === value ? palPrimary.view : pal.view,
|
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}>
|
<Text style={current === value ? palPrimary.text : pal.text}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -86,7 +86,10 @@ export function Component({}: {}) {
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mt20}
|
style={styles.mt20}
|
||||||
onPress={onPressSendEmail}>
|
onPress={onPressSendEmail}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Send email"
|
||||||
|
accessibilityHint="Sends email with confirmation code for account deletion">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[
|
colors={[
|
||||||
gradients.blueLight.start,
|
gradients.blueLight.start,
|
||||||
|
@ -102,7 +105,11 @@ export function Component({}: {}) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.btn, s.mt10]}
|
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}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</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
|
Check your inbox for an email with the confirmation code to enter
|
||||||
below:
|
below:
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -123,8 +134,11 @@ export function Component({}: {}) {
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
value={confirmCode}
|
value={confirmCode}
|
||||||
onChangeText={setConfirmCode}
|
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:
|
Please enter your password as well:
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -135,6 +149,9 @@ export function Component({}: {}) {
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
|
accessibilityLabelledBy="password"
|
||||||
|
accessibilityLabel="Password"
|
||||||
|
accessibilityHint="Input password for account deletion"
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={styles.mt20}>
|
<View style={styles.mt20}>
|
||||||
|
@ -149,14 +166,21 @@ export function Component({}: {}) {
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.btn, styles.evilBtn, styles.mt20]}
|
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]}>
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
Delete my account
|
Delete my account
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.btn, s.mt10]}
|
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}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -175,6 +175,9 @@ export function Component({
|
||||||
onChangeText={v =>
|
onChangeText={v =>
|
||||||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||||
}
|
}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Display name"
|
||||||
|
accessibilityHint="Edit your display name"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
|
@ -188,6 +191,9 @@ export function Component({
|
||||||
multiline
|
multiline
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Description"
|
||||||
|
accessibilityHint="Edit your profile description"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
|
@ -198,7 +204,10 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="editProfileSaveBtn"
|
testID="editProfileSaveBtn"
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPressSave}>
|
onPress={onPressSave}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Save"
|
||||||
|
accessibilityHint="Saves any changes to your profile">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
@ -211,7 +220,11 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="editProfileCancelBtn"
|
testID="editProfileCancelBtn"
|
||||||
style={s.mt5}
|
style={s.mt5}
|
||||||
onPress={onPressCancel}>
|
onPress={onPressCancel}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Cancel profile editing"
|
||||||
|
accessibilityHint=""
|
||||||
|
onAccessibilityEscape={onPressCancel}>
|
||||||
<View style={[styles.btn]}>
|
<View style={[styles.btn]}>
|
||||||
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -87,6 +87,7 @@ const InviteCode = observer(
|
||||||
({testID, code, used}: {testID: string; code: string; used?: boolean}) => {
|
({testID, code, used}: {testID: string; code: string; used?: boolean}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {invitesAvailable} = store.me
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
Clipboard.setString(code)
|
Clipboard.setString(code)
|
||||||
|
@ -98,7 +99,14 @@ const InviteCode = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[styles.inviteCode, pal.border]}
|
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
|
<Text
|
||||||
testID={`${testID}-code`}
|
testID={`${testID}-code`}
|
||||||
type={used ? 'md' : 'md-bold'}
|
type={used ? 'md' : 'md-bold'}
|
||||||
|
|
|
@ -53,6 +53,7 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
store.shell.closeModal()
|
store.shell.closeModal()
|
||||||
}
|
}
|
||||||
const onInnerPress = () => {
|
const onInnerPress = () => {
|
||||||
|
// TODO: can we use prevent default?
|
||||||
// do nothing, we just want to stop it from bubbling
|
// do nothing, we just want to stop it from bubbling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,8 +93,10 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line
|
||||||
<TouchableWithoutFeedback onPress={onPressMask}>
|
<TouchableWithoutFeedback onPress={onPressMask}>
|
||||||
<View style={styles.mask}>
|
<View style={styles.mask}>
|
||||||
|
{/* eslint-disable-next-line */}
|
||||||
<TouchableWithoutFeedback onPress={onInnerPress}>
|
<TouchableWithoutFeedback onPress={onInnerPress}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -110,7 +110,10 @@ export function Component({did}: {did: string}) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="sendReportBtn"
|
testID="sendReportBtn"
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPress}>
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Report account"
|
||||||
|
accessibilityHint={`Reports account with reason ${issue}`}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -153,7 +153,10 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="sendReportBtn"
|
testID="sendReportBtn"
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPress}>
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Report post"
|
||||||
|
accessibilityHint={`Reports post with reason ${issue}`}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export function Component({
|
||||||
onRepost: () => void
|
onRepost: () => void
|
||||||
onQuote: () => void
|
onQuote: () => void
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
|
// TODO: Add author into component
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -31,7 +32,10 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="repostBtn"
|
testID="repostBtn"
|
||||||
style={[styles.actionBtn]}
|
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} />
|
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
{!isReposted ? 'Repost' : 'Undo repost'}
|
{!isReposted ? 'Repost' : 'Undo repost'}
|
||||||
|
@ -40,14 +44,23 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="quoteBtn"
|
testID="quoteBtn"
|
||||||
style={[styles.actionBtn]}
|
style={[styles.actionBtn]}
|
||||||
onPress={onQuote}>
|
onPress={onQuote}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Quote post"
|
||||||
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
Quote Post
|
Quote Post
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity testID="cancelBtn" onPress={onPress}>
|
<TouchableOpacity
|
||||||
|
testID="cancelBtn"
|
||||||
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Cancel quote post"
|
||||||
|
accessibilityHint=""
|
||||||
|
onAccessibilityEscape={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -41,7 +41,8 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="localDevServerButton"
|
testID="localDevServerButton"
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => doSelect(LOCAL_DEV_SERVICE)}>
|
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
|
||||||
|
accessibilityRole="button">
|
||||||
<Text style={styles.btnText}>Local dev server</Text>
|
<Text style={styles.btnText}>Local dev server</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
|
@ -50,7 +51,8 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => doSelect(STAGING_SERVICE)}>
|
onPress={() => doSelect(STAGING_SERVICE)}
|
||||||
|
accessibilityRole="button">
|
||||||
<Text style={styles.btnText}>Staging</Text>
|
<Text style={styles.btnText}>Staging</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
|
@ -61,7 +63,10 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.btn}
|
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>
|
<Text style={styles.btnText}>Bluesky.Social</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
|
@ -83,11 +88,23 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
value={customUrl}
|
value={customUrl}
|
||||||
onChangeText={setCustomUrl}
|
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
|
<TouchableOpacity
|
||||||
testID="customServerSelectBtn"
|
testID="customServerSelectBtn"
|
||||||
style={[pal.borderDark, pal.text, styles.textInputBtn]}
|
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
|
<FontAwesomeIcon
|
||||||
icon="check"
|
icon="check"
|
||||||
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}
|
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}
|
||||||
|
|
|
@ -77,6 +77,9 @@ export function Component({}: {}) {
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
value={email}
|
value={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Email"
|
||||||
|
accessibilityHint="Input your email to get on the Bluesky waitlist"
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={s.mt10}>
|
<View style={s.mt10}>
|
||||||
|
@ -99,7 +102,10 @@ export function Component({}: {}) {
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity onPress={onPressSignup}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressSignup}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityHint={`Confirms signing up ${email} to the waitlist`}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
@ -110,7 +116,13 @@ export function Component({}: {}) {
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</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}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -4,12 +4,13 @@ import ImageEditor from 'react-avatar-editor'
|
||||||
import {Slider} from '@miblanchard/react-native-slider'
|
import {Slider} from '@miblanchard/react-native-slider'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
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 {getDataUriSize} from 'lib/media/util'
|
||||||
import {s, gradients} from 'lib/styles'
|
import {s, gradients} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
|
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
|
||||||
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
|
|
||||||
enum AspectRatio {
|
enum AspectRatio {
|
||||||
Square = 'square',
|
Square = 'square',
|
||||||
|
@ -30,7 +31,7 @@ export function Component({
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
uri: string
|
uri: string
|
||||||
onSelect: (img?: Image) => void
|
onSelect: (img?: RNImage) => void
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -92,19 +93,31 @@ export function Component({
|
||||||
maximumValue={3}
|
maximumValue={3}
|
||||||
containerStyle={styles.slider}
|
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
|
<RectWideIcon
|
||||||
size={24}
|
size={24}
|
||||||
style={as === AspectRatio.Wide ? s.blue3 : undefined}
|
style={as === AspectRatio.Wide ? s.blue3 : undefined}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={doSetAs(AspectRatio.Tall)}>
|
<TouchableOpacity
|
||||||
|
onPress={doSetAs(AspectRatio.Tall)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Tall"
|
||||||
|
accessibilityHint="Sets image aspect ratio to tall">
|
||||||
<RectTallIcon
|
<RectTallIcon
|
||||||
size={24}
|
size={24}
|
||||||
style={as === AspectRatio.Tall ? s.blue3 : undefined}
|
style={as === AspectRatio.Tall ? s.blue3 : undefined}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={doSetAs(AspectRatio.Square)}>
|
<TouchableOpacity
|
||||||
|
onPress={doSetAs(AspectRatio.Square)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Square"
|
||||||
|
accessibilityHint="Sets image aspect ratio to square">
|
||||||
<SquareIcon
|
<SquareIcon
|
||||||
size={24}
|
size={24}
|
||||||
style={as === AspectRatio.Square ? s.blue3 : undefined}
|
style={as === AspectRatio.Square ? s.blue3 : undefined}
|
||||||
|
@ -112,13 +125,21 @@ export function Component({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.btns}>
|
<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}>
|
<Text type="xl" style={pal.link}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<TouchableOpacity onPress={onPressDone}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressDone}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Save image crop"
|
||||||
|
accessibilityHint="Saves image crop settings">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -123,7 +123,8 @@ export const FeedItem = observer(function ({
|
||||||
testID={`feedItem-by-${item.author.handle}`}
|
testID={`feedItem-by-${item.author.handle}`}
|
||||||
href={itemHref}
|
href={itemHref}
|
||||||
title={itemTitle}
|
title={itemTitle}
|
||||||
noFeedback>
|
noFeedback
|
||||||
|
accessible={false}>
|
||||||
<Post
|
<Post
|
||||||
uri={item.uri}
|
uri={item.uri}
|
||||||
initView={item.additionalPost}
|
initView={item.additionalPost}
|
||||||
|
@ -163,6 +164,7 @@ export const FeedItem = observer(function ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line
|
||||||
<Link
|
<Link
|
||||||
testID={`feedItem-by-${item.author.handle}`}
|
testID={`feedItem-by-${item.author.handle}`}
|
||||||
style={[
|
style={[
|
||||||
|
@ -178,8 +180,11 @@ export const FeedItem = observer(function ({
|
||||||
]}
|
]}
|
||||||
href={itemHref}
|
href={itemHref}
|
||||||
title={itemTitle}
|
title={itemTitle}
|
||||||
noFeedback>
|
noFeedback
|
||||||
|
accessible={(item.isLike && authors.length === 1) || item.isRepost}>
|
||||||
<View style={styles.layoutIcon}>
|
<View style={styles.layoutIcon}>
|
||||||
|
{/* TODO: Prevent conditional rendering and move toward composable
|
||||||
|
notifications for clearer accessibility labeling */}
|
||||||
{icon === 'HeartIconSolid' ? (
|
{icon === 'HeartIconSolid' ? (
|
||||||
<HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} />
|
<HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -192,17 +197,18 @@ export const FeedItem = observer(function ({
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}>
|
onPress={authors.length > 1 ? onToggleAuthorsExpanded : undefined}
|
||||||
|
accessible={false}>
|
||||||
<CondensedAuthorsList
|
<CondensedAuthorsList
|
||||||
visible={!isAuthorsExpanded}
|
visible={!isAuthorsExpanded}
|
||||||
authors={authors}
|
authors={authors}
|
||||||
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
|
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
|
||||||
/>
|
/>
|
||||||
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
|
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
|
||||||
<View style={styles.meta}>
|
<Text style={styles.meta}>
|
||||||
<TextLink
|
<TextLink
|
||||||
key={authors[0].href}
|
key={authors[0].href}
|
||||||
style={[pal.text, s.bold, styles.metaItem]}
|
style={[pal.text, s.bold]}
|
||||||
href={authors[0].href}
|
href={authors[0].href}
|
||||||
text={sanitizeDisplayName(
|
text={sanitizeDisplayName(
|
||||||
authors[0].displayName || authors[0].handle,
|
authors[0].displayName || authors[0].handle,
|
||||||
|
@ -210,17 +216,15 @@ export const FeedItem = observer(function ({
|
||||||
/>
|
/>
|
||||||
{authors.length > 1 ? (
|
{authors.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
<Text style={[styles.metaItem, pal.text]}>and</Text>
|
<Text style={[pal.text]}> and </Text>
|
||||||
<Text style={[styles.metaItem, pal.text, s.bold]}>
|
<Text style={[pal.text, s.bold]}>
|
||||||
{authors.length - 1} {pluralize(authors.length - 1, 'other')}
|
{authors.length - 1} {pluralize(authors.length - 1, 'other')}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Text style={[styles.metaItem, pal.text]}>{action}</Text>
|
<Text style={[pal.text]}> {action}</Text>
|
||||||
<Text style={[styles.metaItem, pal.textLight]}>
|
<Text style={[pal.textLight]}> {ago(item.indexedAt)}</Text>
|
||||||
{ago(item.indexedAt)}
|
</Text>
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{item.isLike || item.isRepost || item.isQuote ? (
|
{item.isLike || item.isRepost || item.isQuote ? (
|
||||||
<AdditionalPostText additionalPost={item.additionalPost} />
|
<AdditionalPostText additionalPost={item.additionalPost} />
|
||||||
|
@ -245,7 +249,10 @@ function CondensedAuthorsList({
|
||||||
<View style={styles.avis}>
|
<View style={styles.avis}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.expandedAuthorsCloseBtn}
|
style={styles.expandedAuthorsCloseBtn}
|
||||||
onPress={onToggleAuthorsExpanded}>
|
onPress={onToggleAuthorsExpanded}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Hide user list"
|
||||||
|
accessibilityHint="Collapses list of users for a given notification">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-up"
|
icon="angle-up"
|
||||||
size={18}
|
size={18}
|
||||||
|
@ -276,27 +283,32 @@ function CondensedAuthorsList({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={styles.avis}>
|
<TouchableOpacity
|
||||||
{authors.slice(0, MAX_AUTHORS).map(author => (
|
accessibilityLabel="Show users"
|
||||||
<View key={author.href} style={s.mr5}>
|
accessibilityHint="Opens an expanded list of users in this notification"
|
||||||
<UserAvatar
|
onPress={onToggleAuthorsExpanded}>
|
||||||
size={35}
|
<View style={styles.avis}>
|
||||||
avatar={author.avatar}
|
{authors.slice(0, MAX_AUTHORS).map(author => (
|
||||||
moderation={author.moderation.avatar}
|
<View key={author.href} style={s.mr5}>
|
||||||
/>
|
<UserAvatar
|
||||||
</View>
|
size={35}
|
||||||
))}
|
avatar={author.avatar}
|
||||||
{authors.length > MAX_AUTHORS ? (
|
moderation={author.moderation.avatar}
|
||||||
<Text style={[styles.aviExtraCount, pal.textLight]}>
|
/>
|
||||||
+{authors.length - MAX_AUTHORS}
|
</View>
|
||||||
</Text>
|
))}
|
||||||
) : undefined}
|
{authors.length > MAX_AUTHORS ? (
|
||||||
<FontAwesomeIcon
|
<Text style={[styles.aviExtraCount, pal.textLight]}>
|
||||||
icon="angle-down"
|
+{authors.length - MAX_AUTHORS}
|
||||||
size={18}
|
</Text>
|
||||||
style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]}
|
) : undefined}
|
||||||
/>
|
<FontAwesomeIcon
|
||||||
</View>
|
icon="angle-down"
|
||||||
|
size={18}
|
||||||
|
style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,9 +438,6 @@ const styles = StyleSheet.create({
|
||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 2,
|
paddingBottom: 2,
|
||||||
},
|
},
|
||||||
metaItem: {
|
|
||||||
paddingRight: 3,
|
|
||||||
},
|
|
||||||
postText: {
|
postText: {
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
color: colors.black,
|
color: colors.black,
|
||||||
|
|
|
@ -37,7 +37,10 @@ export const FeedsTabBar = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="viewHeaderDrawerBtn"
|
testID="viewHeaderDrawerBtn"
|
||||||
style={styles.tabBarAvi}
|
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} />
|
<UserAvatar avatar={store.me.avatar} size={30} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TabBar
|
<TabBar
|
||||||
|
|
|
@ -180,7 +180,11 @@ export const PostThread = observer(function PostThread({
|
||||||
<Text type="md" style={[pal.text, s.mb10]}>
|
<Text type="md" style={[pal.text, s.mb10]}>
|
||||||
The post may have been deleted.
|
The post may have been deleted.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={onPressBack}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressBack}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Go back"
|
||||||
|
accessibilityHint="Navigates to the previous screen">
|
||||||
<Text type="2xl" style={pal.link}>
|
<Text type="2xl" style={pal.link}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
|
@ -210,7 +214,11 @@ export const PostThread = observer(function PostThread({
|
||||||
<Text type="md" style={[pal.text, s.mb10]}>
|
<Text type="md" style={[pal.text, s.mb10]}>
|
||||||
You have blocked the author or you have been blocked by the author.
|
You have blocked the author or you have been blocked by the author.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={onPressBack}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressBack}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Go back"
|
||||||
|
accessibilityHint="Navigates to the previous screen">
|
||||||
<Text type="2xl" style={pal.link}>
|
<Text type="2xl" style={pal.link}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
|
|
|
@ -151,7 +151,12 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
moderation={item.moderation.thread}>
|
moderation={item.moderation.thread}>
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<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
|
<UserAvatar
|
||||||
size={52}
|
size={52}
|
||||||
avatar={item.post.author.avatar}
|
avatar={item.post.author.avatar}
|
||||||
|
@ -183,7 +188,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<PostDropdownBtn
|
<PostDropdownBtn
|
||||||
testID="postDropdownBtn"
|
testID="postDropdownBtn"
|
||||||
style={styles.metaItem}
|
style={[styles.metaItem, s.mt2, s.px5]}
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
itemCid={itemCid}
|
itemCid={itemCid}
|
||||||
itemHref={itemHref}
|
itemHref={itemHref}
|
||||||
|
@ -197,7 +202,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="ellipsis-h"
|
icon="ellipsis-h"
|
||||||
size={14}
|
size={14}
|
||||||
style={[s.mt2, s.mr5, pal.textLight]}
|
style={[pal.textLight]}
|
||||||
/>
|
/>
|
||||||
</PostDropdownBtn>
|
</PostDropdownBtn>
|
||||||
</View>
|
</View>
|
||||||
|
@ -435,10 +440,10 @@ const styles = StyleSheet.create({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
layoutAvi: {
|
layoutAvi: {
|
||||||
width: 70,
|
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
layoutContent: {
|
layoutContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
@ -282,7 +282,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="profileHeaderEditProfileButton"
|
testID="profileHeaderEditProfileButton"
|
||||||
onPress={onPressEditProfile}
|
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}>
|
<Text type="button" style={pal.text}>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -291,7 +294,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="unblockBtn"
|
testID="unblockBtn"
|
||||||
onPress={onPressUnblockAccount}
|
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]}>
|
<Text type="button" style={[pal.text, s.bold]}>
|
||||||
Unblock
|
Unblock
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -303,7 +309,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="unfollowBtn"
|
testID="unfollowBtn"
|
||||||
onPress={onPressToggleFollow}
|
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
|
<FontAwesomeIcon
|
||||||
icon="check"
|
icon="check"
|
||||||
style={[pal.text, s.mr5]}
|
style={[pal.text, s.mr5]}
|
||||||
|
@ -317,7 +326,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="followBtn"
|
testID="followBtn"
|
||||||
onPress={onPressToggleFollow}
|
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
|
<FontAwesomeIcon
|
||||||
icon="plus"
|
icon="plus"
|
||||||
style={[s.white as FontAwesomeIconStyle, s.mr5]}
|
style={[s.white as FontAwesomeIconStyle, s.mr5]}
|
||||||
|
@ -363,7 +375,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="profileHeaderFollowersButton"
|
testID="profileHeaderFollowersButton"
|
||||||
style={[s.flexRow, s.mr10]}
|
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]}>
|
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
|
||||||
{formatCount(view.followersCount)}
|
{formatCount(view.followersCount)}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -374,7 +389,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="profileHeaderFollowsButton"
|
testID="profileHeaderFollowsButton"
|
||||||
style={[s.flexRow, s.mr10]}
|
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]}>
|
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
|
||||||
{formatCount(view.followsCount)}
|
{formatCount(view.followsCount)}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -382,14 +400,12 @@ const ProfileHeaderLoaded = observer(
|
||||||
following
|
following
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={[s.flexRow, s.mr10]}>
|
<Text type="md" style={[s.bold, pal.text]}>
|
||||||
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
|
{view.postsCount}{' '}
|
||||||
{view.postsCount}
|
|
||||||
</Text>
|
|
||||||
<Text type="md" style={[pal.textLight]}>
|
<Text type="md" style={[pal.textLight]}>
|
||||||
{pluralize(view.postsCount, 'post')}
|
{pluralize(view.postsCount, 'post')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{view.descriptionRichText ? (
|
{view.descriptionRichText ? (
|
||||||
<RichText
|
<RichText
|
||||||
|
@ -440,7 +456,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
{!isDesktopWeb && !hideBackButton && (
|
{!isDesktopWeb && !hideBackButton && (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
hitSlop={BACK_HITSLOP}>
|
hitSlop={BACK_HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Go back"
|
||||||
|
accessibilityHint="Navigates to the previous screen">
|
||||||
<View style={styles.backBtnWrapper}>
|
<View style={styles.backBtnWrapper}>
|
||||||
<BlurView style={styles.backBtn} blurType="dark">
|
<BlurView style={styles.backBtn} blurType="dark">
|
||||||
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
||||||
|
@ -450,7 +469,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
)}
|
)}
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
testID="profileHeaderAviButton"
|
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
|
<View
|
||||||
style={[
|
style={[
|
||||||
pal.view,
|
pal.view,
|
||||||
|
|
|
@ -54,7 +54,9 @@ export function HeaderWithInput({
|
||||||
testID="viewHeaderBackOrMenuBtn"
|
testID="viewHeaderBackOrMenuBtn"
|
||||||
onPress={onPressMenu}
|
onPress={onPressMenu}
|
||||||
hitSlop={MENU_HITSLOP}
|
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} />
|
<UserAvatar size={30} avatar={store.me.avatar} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View
|
<View
|
||||||
|
@ -80,9 +82,15 @@ export function HeaderWithInput({
|
||||||
onBlur={() => setIsInputFocused(false)}
|
onBlur={() => setIsInputFocused(false)}
|
||||||
onChangeText={onChangeQuery}
|
onChangeText={onChangeQuery}
|
||||||
onSubmitEditing={onSubmitQuery}
|
onSubmitEditing={onSubmitQuery}
|
||||||
|
autoFocus={true}
|
||||||
|
accessibilityRole="search"
|
||||||
/>
|
/>
|
||||||
{query ? (
|
{query ? (
|
||||||
<TouchableOpacity onPress={onPressClearQuery}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressClearQuery}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Clear search query"
|
||||||
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="xmark"
|
icon="xmark"
|
||||||
size={16}
|
size={16}
|
||||||
|
@ -93,7 +101,9 @@ export function HeaderWithInput({
|
||||||
</View>
|
</View>
|
||||||
{query || isInputFocused ? (
|
{query || isInputFocused ? (
|
||||||
<View style={styles.headerCancelBtn}>
|
<View style={styles.headerCancelBtn}>
|
||||||
<TouchableOpacity onPress={onPressCancelSearchInner}>
|
<TouchableOpacity
|
||||||
|
onPress={onPressCancelSearchInner}
|
||||||
|
accessibilityRole="button">
|
||||||
<Text style={pal.text}>Cancel</Text>
|
<Text style={pal.text}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -110,9 +120,10 @@ const styles = StyleSheet.create({
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
},
|
},
|
||||||
headerMenuBtn: {
|
headerMenuBtn: {
|
||||||
width: 40,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
marginLeft: 6,
|
borderRadius: 30,
|
||||||
|
marginHorizontal: 6,
|
||||||
},
|
},
|
||||||
headerSearchContainer: {
|
headerSearchContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native'
|
import {TouchableWithoutFeedback} from 'react-native'
|
||||||
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
|
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
|
@ -8,7 +8,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
export function createCustomBackdrop(
|
export function createCustomBackdrop(
|
||||||
onClose?: ((event: GestureResponderEvent) => void) | undefined,
|
onClose?: (() => void) | undefined,
|
||||||
): React.FC<BottomSheetBackdropProps> {
|
): React.FC<BottomSheetBackdropProps> {
|
||||||
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
|
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
|
||||||
// animated variables
|
// animated variables
|
||||||
|
@ -27,7 +27,15 @@ export function createCustomBackdrop(
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={onClose}>
|
<TouchableWithoutFeedback
|
||||||
|
onPress={onClose}
|
||||||
|
accessibilityLabel="Close bottom drawer"
|
||||||
|
accessibilityHint=""
|
||||||
|
onAccessibilityEscape={() => {
|
||||||
|
if (onClose !== undefined) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<Animated.View style={containerStyle} />
|
<Animated.View style={containerStyle} />
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, {ComponentProps} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
|
@ -29,6 +29,16 @@ type Event =
|
||||||
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
| GestureResponderEvent
|
| 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({
|
export const Link = observer(function Link({
|
||||||
testID,
|
testID,
|
||||||
style,
|
style,
|
||||||
|
@ -37,15 +47,9 @@ export const Link = observer(function Link({
|
||||||
children,
|
children,
|
||||||
noFeedback,
|
noFeedback,
|
||||||
asAnchor,
|
asAnchor,
|
||||||
}: {
|
accessible,
|
||||||
testID?: string
|
...props
|
||||||
style?: StyleProp<ViewStyle>
|
}: Props) {
|
||||||
href?: string
|
|
||||||
title?: string
|
|
||||||
children?: React.ReactNode
|
|
||||||
noFeedback?: boolean
|
|
||||||
asAnchor?: boolean
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
@ -64,7 +68,10 @@ export const Link = observer(function Link({
|
||||||
testID={testID}
|
testID={testID}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
href={asAnchor ? sanitizeUrl(href) : undefined}>
|
href={asAnchor ? sanitizeUrl(href) : undefined}
|
||||||
|
accessible={accessible}
|
||||||
|
accessibilityRole="link"
|
||||||
|
{...props}>
|
||||||
<View style={style}>
|
<View style={style}>
|
||||||
{children ? children : <Text>{title || 'link'}</Text>}
|
{children ? children : <Text>{title || 'link'}</Text>}
|
||||||
</View>
|
</View>
|
||||||
|
@ -76,8 +83,11 @@ export const Link = observer(function Link({
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={style}
|
style={style}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
accessible={accessible}
|
||||||
|
accessibilityRole="link"
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
href={asAnchor ? sanitizeUrl(href) : undefined}>
|
href={asAnchor ? sanitizeUrl(href) : undefined}
|
||||||
|
{...props}>
|
||||||
{children ? children : <Text>{title || 'link'}</Text>}
|
{children ? children : <Text>{title || 'link'}</Text>}
|
||||||
</TouchableOpacity>
|
</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 (
|
return (
|
||||||
<View style={[styles.ctrls, opts.style]}>
|
<View style={[styles.ctrls, opts.style]}>
|
||||||
<View>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
testID="replyBtn"
|
||||||
testID="replyBtn"
|
style={styles.ctrl}
|
||||||
style={styles.ctrl}
|
hitSlop={HITSLOP}
|
||||||
hitSlop={HITSLOP}
|
onPress={opts.onPressReply}
|
||||||
onPress={opts.onPressReply}>
|
accessibilityRole="button"
|
||||||
<CommentBottomArrow
|
accessibilityLabel="Reply"
|
||||||
style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
|
accessibilityHint="Opens reply composer">
|
||||||
strokeWidth={3}
|
<CommentBottomArrow
|
||||||
size={opts.big ? 20 : 15}
|
style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
|
||||||
/>
|
strokeWidth={3}
|
||||||
{typeof opts.replyCount !== 'undefined' ? (
|
size={opts.big ? 20 : 15}
|
||||||
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
|
/>
|
||||||
{opts.replyCount}
|
{typeof opts.replyCount !== 'undefined' ? (
|
||||||
</Text>
|
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
|
||||||
) : undefined}
|
{opts.replyCount}
|
||||||
</TouchableOpacity>
|
</Text>
|
||||||
</View>
|
) : undefined}
|
||||||
<View>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="repostBtn"
|
testID="repostBtn"
|
||||||
hitSlop={HITSLOP}
|
hitSlop={HITSLOP}
|
||||||
onPress={onPressToggleRepostWrapper}
|
onPress={onPressToggleRepostWrapper}
|
||||||
style={styles.ctrl}>
|
style={styles.ctrl}
|
||||||
<RepostIcon
|
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={
|
style={
|
||||||
opts.isReposted
|
opts.isReposted
|
||||||
? (styles.ctrlIconReposted as StyleProp<ViewStyle>)
|
? [s.bold, s.green3, s.f15, s.ml5]
|
||||||
: defaultCtrlColor
|
: [defaultCtrlColor, s.f15, s.ml5]
|
||||||
}
|
}>
|
||||||
strokeWidth={2.4}
|
{opts.repostCount}
|
||||||
size={opts.big ? 24 : 20}
|
</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
|
<HeartIcon
|
||||||
testID="repostCount"
|
style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
|
||||||
style={
|
strokeWidth={3}
|
||||||
opts.isReposted
|
size={opts.big ? 20 : 16}
|
||||||
? [s.bold, s.green3, s.f15, s.ml5]
|
/>
|
||||||
: [defaultCtrlColor, s.f15, s.ml5]
|
)}
|
||||||
}>
|
{typeof opts.likeCount !== 'undefined' ? (
|
||||||
{opts.repostCount}
|
<Text
|
||||||
</Text>
|
testID="likeCount"
|
||||||
) : undefined}
|
style={
|
||||||
</TouchableOpacity>
|
opts.isLiked
|
||||||
</View>
|
? [s.bold, s.red3, s.f15, s.ml5]
|
||||||
<View>
|
: [defaultCtrlColor, s.f15, s.ml5]
|
||||||
<TouchableOpacity
|
}>
|
||||||
testID="likeBtn"
|
{opts.likeCount}
|
||||||
style={styles.ctrl}
|
</Text>
|
||||||
hitSlop={HITSLOP}
|
) : undefined}
|
||||||
onPress={onPressToggleLikeWrapper}>
|
</TouchableOpacity>
|
||||||
{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>
|
|
||||||
<View>
|
<View>
|
||||||
{opts.big ? undefined : (
|
{opts.big ? undefined : (
|
||||||
<PostDropdownBtn
|
<PostDropdownBtn
|
||||||
|
|
|
@ -85,6 +85,8 @@ export function Selector({
|
||||||
onSelect?.(index)
|
onSelect?.(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numItems = items.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[pal.view, styles.outer]}
|
style={[pal.view, styles.outer]}
|
||||||
|
@ -97,7 +99,9 @@ export function Selector({
|
||||||
<Pressable
|
<Pressable
|
||||||
testID={`selector-${i}`}
|
testID={`selector-${i}`}
|
||||||
key={item}
|
key={item}
|
||||||
onPress={() => onPressItem(i)}>
|
onPress={() => onPressItem(i)}
|
||||||
|
accessibilityLabel={`Select ${item}`}
|
||||||
|
accessibilityHint={`Select option ${i} of ${numItems}`}>
|
||||||
<View style={styles.item} ref={itemRefs[i]}>
|
<View style={styles.item} ref={itemRefs[i]}>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
|
|
|
@ -150,6 +150,7 @@ export function UserAvatar({
|
||||||
borderRadius: Math.floor(size / 2),
|
borderRadius: Math.floor(size / 2),
|
||||||
}}
|
}}
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar}}
|
||||||
|
accessibilityRole="image"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DefaultAvatar size={size} />
|
<DefaultAvatar size={size} />
|
||||||
|
@ -167,7 +168,11 @@ export function UserAvatar({
|
||||||
<View style={{width: size, height: size}}>
|
<View style={{width: size, height: size}}>
|
||||||
<HighPriorityImage
|
<HighPriorityImage
|
||||||
testID="userAvatarImage"
|
testID="userAvatarImage"
|
||||||
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: Math.floor(size / 2),
|
||||||
|
}}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar}}
|
||||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
import {Image as TImage} from 'lib/media/types'
|
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
|
@ -15,6 +14,7 @@ import {DropdownButton} from './forms/DropdownButton'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {AvatarModeration} from 'lib/labeling/types'
|
import {AvatarModeration} from 'lib/labeling/types'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
|
|
||||||
export function UserBanner({
|
export function UserBanner({
|
||||||
banner,
|
banner,
|
||||||
|
@ -23,7 +23,7 @@ export function UserBanner({
|
||||||
}: {
|
}: {
|
||||||
banner?: string | null
|
banner?: string | null
|
||||||
moderation?: AvatarModeration
|
moderation?: AvatarModeration
|
||||||
onSelectNewBanner?: (img: TImage | null) => void
|
onSelectNewBanner?: (img: RNImage | null) => void
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -94,6 +94,8 @@ export function UserBanner({
|
||||||
testID="userBannerImage"
|
testID="userBannerImage"
|
||||||
style={styles.bannerImage}
|
style={styles.bannerImage}
|
||||||
source={{uri: banner}}
|
source={{uri: banner}}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
|
@ -118,6 +120,8 @@ export function UserBanner({
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
source={{uri: banner}}
|
source={{uri: banner}}
|
||||||
blurRadius={moderation?.blur ? 100 : 0}
|
blurRadius={moderation?.blur ? 100 : 0}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -60,7 +60,14 @@ export const ViewHeader = observer(function ({
|
||||||
testID="viewHeaderDrawerBtn"
|
testID="viewHeaderDrawerBtn"
|
||||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||||
hitSlop={BACK_HITSLOP}
|
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 ? (
|
{canGoBack ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
size={18}
|
size={18}
|
||||||
|
@ -171,9 +178,9 @@ const styles = StyleSheet.create({
|
||||||
height: 30,
|
height: 30,
|
||||||
},
|
},
|
||||||
backBtnWide: {
|
backBtnWide: {
|
||||||
width: 40,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
marginLeft: 6,
|
paddingHorizontal: 6,
|
||||||
},
|
},
|
||||||
backIcon: {
|
backIcon: {
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
|
|
|
@ -132,7 +132,12 @@ export function Selector({
|
||||||
<Pressable
|
<Pressable
|
||||||
testID={`selector-${i}`}
|
testID={`selector-${i}`}
|
||||||
key={item}
|
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
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.item,
|
styles.item,
|
||||||
|
|
|
@ -47,7 +47,10 @@ export function ErrorMessage({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="errorMessageTryAgainButton"
|
testID="errorMessageTryAgainButton"
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={onPressTryAgain}>
|
onPress={onPressTryAgain}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Retry"
|
||||||
|
accessibilityHint="Retries the last action, which errored out">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="arrows-rotate"
|
||||||
style={{color: theme.palette.error.icon}}
|
style={{color: theme.palette.error.icon}}
|
||||||
|
|
|
@ -57,7 +57,9 @@ export function ErrorScreen({
|
||||||
testID="errorScreenTryAgainButton"
|
testID="errorScreenTryAgainButton"
|
||||||
type="default"
|
type="default"
|
||||||
style={[styles.btn]}
|
style={[styles.btn]}
|
||||||
onPress={onPressTryAgain}>
|
onPress={onPressTryAgain}
|
||||||
|
accessibilityLabel="Retry"
|
||||||
|
accessibilityHint="Retries the last action, which errored out">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="arrows-rotate"
|
||||||
style={pal.link as FontAwesomeIconStyle}
|
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 {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native'
|
||||||
Animated,
|
|
||||||
GestureResponderEvent,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {gradients} from 'lib/styles'
|
import {gradients} from 'lib/styles'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {isMobileWeb} from 'platform/detection'
|
import {isMobileWeb} from 'platform/detection'
|
||||||
|
|
||||||
type OnPress = ((event: GestureResponderEvent) => void) | undefined
|
export interface FABProps
|
||||||
export interface FABProps {
|
extends ComponentProps<typeof TouchableWithoutFeedback> {
|
||||||
testID?: string
|
testID?: string
|
||||||
icon: JSX.Element
|
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 store = useStores()
|
||||||
const interp = useAnimatedValue(0)
|
const interp = useAnimatedValue(0)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -34,7 +28,7 @@ export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
|
||||||
transform: [{translateY: Animated.multiply(interp, 60)}],
|
transform: [{translateY: Animated.multiply(interp, 60)}],
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback testID={testID} onPress={onPress}>
|
<TouchableWithoutFeedback testID={testID} {...props}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.outer, isMobileWeb && styles.mobileWebOuter, transform]}>
|
style={[styles.outer, isMobileWeb && styles.mobileWebOuter, transform]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
|
|
@ -26,6 +26,7 @@ export type ButtonType =
|
||||||
| 'secondary-light'
|
| 'secondary-light'
|
||||||
| 'default-light'
|
| 'default-light'
|
||||||
|
|
||||||
|
// TODO: Enforce that button always has a label
|
||||||
export function Button({
|
export function Button({
|
||||||
type = 'primary',
|
type = 'primary',
|
||||||
label,
|
label,
|
||||||
|
@ -131,7 +132,8 @@ export function Button({
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[typeOuterStyle, styles.outer, style]}
|
style={[typeOuterStyle, styles.outer, style]}
|
||||||
onPress={onPressWrapped}
|
onPress={onPressWrapped}
|
||||||
testID={testID}>
|
testID={testID}
|
||||||
|
accessibilityRole="button">
|
||||||
{label ? (
|
{label ? (
|
||||||
<Text type="button" style={[typeLabelStyle, labelStyle]}>
|
<Text type="button" style={[typeLabelStyle, labelStyle]}>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useRef} from 'react'
|
import React, {PropsWithChildren, useMemo, useRef} from 'react'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
@ -39,6 +39,19 @@ type MaybeDropdownItem = DropdownItem | false | undefined
|
||||||
|
|
||||||
export type DropdownButtonType = ButtonType | 'bare'
|
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({
|
export function DropdownButton({
|
||||||
testID,
|
testID,
|
||||||
type = 'bare',
|
type = 'bare',
|
||||||
|
@ -50,18 +63,7 @@ export function DropdownButton({
|
||||||
openToRight = false,
|
openToRight = false,
|
||||||
rightOffset = 0,
|
rightOffset = 0,
|
||||||
bottomOffset = 0,
|
bottomOffset = 0,
|
||||||
}: {
|
}: PropsWithChildren<DropdownButtonProps>) {
|
||||||
testID?: string
|
|
||||||
type?: DropdownButtonType
|
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
items: MaybeDropdownItem[]
|
|
||||||
label?: string
|
|
||||||
menuWidth?: number
|
|
||||||
children?: React.ReactNode
|
|
||||||
openToRight?: boolean
|
|
||||||
rightOffset?: number
|
|
||||||
bottomOffset?: number
|
|
||||||
}) {
|
|
||||||
const ref1 = useRef<TouchableOpacity>(null)
|
const ref1 = useRef<TouchableOpacity>(null)
|
||||||
const ref2 = useRef<View>(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') {
|
if (type === 'bare') {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -112,7 +126,10 @@ export function DropdownButton({
|
||||||
style={style}
|
style={style}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
hitSlop={HITSLOP}
|
hitSlop={HITSLOP}
|
||||||
ref={ref1}>
|
ref={ref1}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Opens ${numItems} options`}
|
||||||
|
accessibilityHint={`Opens ${numItems} options`}>
|
||||||
{children}
|
{children}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
@ -283,9 +300,20 @@ const DropdownItems = ({
|
||||||
const separatorColor =
|
const separatorColor =
|
||||||
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
||||||
|
|
||||||
|
const numItems = items.filter(isBtn).length
|
||||||
|
|
||||||
return (
|
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]} />
|
<View style={[styles.bg]} />
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
<View
|
<View
|
||||||
|
@ -301,7 +329,9 @@ const DropdownItems = ({
|
||||||
testID={item.testID}
|
testID={item.testID}
|
||||||
key={index}
|
key={index}
|
||||||
style={[styles.menuItem]}
|
style={[styles.menuItem]}
|
||||||
onPress={() => onPressItem(index)}>
|
onPress={() => onPressItem(index)}
|
||||||
|
accessibilityLabel={item.label}
|
||||||
|
accessibilityHint={`Option ${index + 1} of ${numItems}`}>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
|
|
|
@ -62,12 +62,17 @@ export function AutoSizedImage({
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
delayPressIn={DELAY_PRESS_IN}
|
delayPressIn={DELAY_PRESS_IN}
|
||||||
style={[styles.container, style]}>
|
style={[styles.container, style]}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Share image"
|
||||||
|
accessibilityHint="Opens ways of sharing image">
|
||||||
<Image
|
<Image
|
||||||
style={[styles.image, {aspectRatio}]}
|
style={[styles.image, {aspectRatio}]}
|
||||||
source={uri}
|
source={uri}
|
||||||
accessible={true} // Must set for `accessibilityLabel` to work
|
accessible={true} // Must set for `accessibilityLabel` to work
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
accessibilityLabel={alt}
|
accessibilityLabel={alt}
|
||||||
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -80,7 +85,9 @@ export function AutoSizedImage({
|
||||||
style={[styles.image, {aspectRatio}]}
|
style={[styles.image, {aspectRatio}]}
|
||||||
source={{uri}}
|
source={{uri}}
|
||||||
accessible={true} // Must set for `accessibilityLabel` to work
|
accessible={true} // Must set for `accessibilityLabel` to work
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
accessibilityLabel={alt}
|
accessibilityLabel={alt}
|
||||||
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -41,16 +41,25 @@ export const GalleryItem: FC<GalleryItemProps> = ({
|
||||||
delayPressIn={DELAY_PRESS_IN}
|
delayPressIn={DELAY_PRESS_IN}
|
||||||
onPress={() => onPress?.(index)}
|
onPress={() => onPress?.(index)}
|
||||||
onPressIn={() => onPressIn?.(index)}
|
onPressIn={() => onPressIn?.(index)}
|
||||||
onLongPress={() => onLongPress?.(index)}>
|
onLongPress={() => onLongPress?.(index)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="View image"
|
||||||
|
accessibilityHint="">
|
||||||
<Image
|
<Image
|
||||||
source={{uri: image.thumb}}
|
source={{uri: image.thumb}}
|
||||||
style={imageStyle}
|
style={imageStyle}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel={image.alt}
|
accessibilityLabel={image.alt}
|
||||||
|
accessibilityHint=""
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{image.alt === '' ? null : (
|
{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>
|
<Text style={styles.alt}>ALT</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,5 +8,7 @@ export function HighPriorityImage({source, ...props}: HighPriorityImageProps) {
|
||||||
const updatedSource = {
|
const updatedSource = {
|
||||||
uri: typeof source === 'object' && source ? source.uri : '',
|
uri: typeof source === 'object' && source ? source.uri : '',
|
||||||
} satisfies ImageSource
|
} 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) {
|
export function ImageHorzList({images, onPress, style}: Props) {
|
||||||
|
const numImages = images.length
|
||||||
return (
|
return (
|
||||||
<View style={[styles.flexRow, style]}>
|
<View style={[styles.flexRow, style]}>
|
||||||
{images.map(({thumb, alt}, i) => (
|
{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
|
<Image
|
||||||
source={{uri: thumb}}
|
source={{uri: thumb}}
|
||||||
style={styles.image}
|
style={styles.image}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel={alt}
|
accessibilityIgnoresInvertColors
|
||||||
|
accessibilityHint={alt}
|
||||||
|
accessibilityLabel=""
|
||||||
/>
|
/>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -23,7 +23,10 @@ export const LoadLatestBtn = ({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[pal.view, pal.borderDark, styles.loadLatest]}
|
style={[pal.view, pal.borderDark, styles.loadLatest]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Load new ${label}`}
|
||||||
|
accessibilityHint="">
|
||||||
<Text type="md-bold" style={pal.text}>
|
<Text type="md-bold" style={pal.text}>
|
||||||
<UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} />
|
<UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} />
|
||||||
Load new {label}
|
Load new {label}
|
||||||
|
|
|
@ -23,7 +23,10 @@ export const LoadLatestBtn = observer(
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Load new ${label}`}
|
||||||
|
accessibilityHint={`Loads new ${label}`}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -55,7 +55,14 @@ export function ContentHider({
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.showBtn}
|
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}>
|
<Text type="md" style={pal.link}>
|
||||||
{override ? 'Hide' : 'Show'}
|
{override ? 'Hide' : 'Show'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -46,7 +46,8 @@ export function PostHider({
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.showBtn}
|
style={styles.showBtn}
|
||||||
onPress={() => setOverride(v => !v)}>
|
onPress={() => setOverride(v => !v)}
|
||||||
|
accessibilityRole="button">
|
||||||
<Text type="md" style={pal.link}>
|
<Text type="md" style={pal.link}>
|
||||||
{override ? 'Hide' : 'Show'} post
|
{override ? 'Hide' : 'Show'} post
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -136,7 +136,10 @@ export function PostEmbeds({
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onPressAltText(alt)
|
onPressAltText(alt)
|
||||||
}}>
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="View alt text"
|
||||||
|
accessibilityHint="Opens modal with alt text">
|
||||||
<Text style={styles.alt}>ALT</Text>
|
<Text style={styles.alt}>ALT</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -184,7 +184,10 @@ function AppPassword({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[styles.item, pal.border]}
|
style={[styles.item, pal.border]}
|
||||||
onPress={onDelete}>
|
onPress={onDelete}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Delete"
|
||||||
|
accessibilityHint="Deletes app password">
|
||||||
<Text type="md-bold" style={pal.text}>
|
<Text type="md-bold" style={pal.text}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -250,7 +253,6 @@ const styles = StyleSheet.create({
|
||||||
pr10: {
|
pr10: {
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
btnContainer: {
|
btnContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
@ -226,6 +226,9 @@ const FeedPage = observer(
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Compose"
|
||||||
|
accessibilityHint="Opens post composer"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -46,7 +46,9 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
||||||
<View key={`entry-${entry.id}`}>
|
<View key={`entry-${entry.id}`}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.entry, pal.border, pal.view]}
|
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' ? (
|
{entry.type === 'debug' ? (
|
||||||
<FontAwesomeIcon icon="info" />
|
<FontAwesomeIcon icon="info" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -118,10 +118,10 @@ export const SearchScreen = withAuthRequired(
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={onPress}>
|
<TouchableWithoutFeedback onPress={onPress} accessible={false}>
|
||||||
<View style={[pal.view, styles.container]}>
|
<View style={[pal.view, styles.container]}>
|
||||||
<HeaderWithInput
|
<HeaderWithInput
|
||||||
isInputFocused={isInputFocused}
|
isInputFocused={true}
|
||||||
query={query}
|
query={query}
|
||||||
setIsInputFocused={setIsInputFocused}
|
setIsInputFocused={setIsInputFocused}
|
||||||
onChangeQuery={onChangeQuery}
|
onChangeQuery={onChangeQuery}
|
||||||
|
|
|
@ -161,7 +161,9 @@ export const SettingsScreen = withAuthRequired(
|
||||||
<Link
|
<Link
|
||||||
href={`/profile/${store.me.handle}`}
|
href={`/profile/${store.me.handle}`}
|
||||||
title="Your profile"
|
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={[pal.view, styles.linkCard]}>
|
||||||
<View style={styles.avi}>
|
<View style={styles.avi}>
|
||||||
<UserAvatar size={40} avatar={store.me.avatar} />
|
<UserAvatar size={40} avatar={store.me.avatar} />
|
||||||
|
@ -176,7 +178,10 @@ export const SettingsScreen = withAuthRequired(
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="signOutBtn"
|
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}>
|
<Text type="lg" style={pal.link}>
|
||||||
Sign out
|
Sign out
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -191,7 +196,10 @@ export const SettingsScreen = withAuthRequired(
|
||||||
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
|
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
|
||||||
onPress={
|
onPress={
|
||||||
isSwitching ? undefined : () => onPressSwitchAccount(account)
|
isSwitching ? undefined : () => onPressSwitchAccount(account)
|
||||||
}>
|
}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Switch to ${account.handle}`}
|
||||||
|
accessibilityHint="Switches the account you are logged in to">
|
||||||
<View style={styles.avi}>
|
<View style={styles.avi}>
|
||||||
<UserAvatar size={40} avatar={account.aviUrl} />
|
<UserAvatar size={40} avatar={account.aviUrl} />
|
||||||
</View>
|
</View>
|
||||||
|
@ -209,7 +217,10 @@ export const SettingsScreen = withAuthRequired(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="switchToNewAccountBtn"
|
testID="switchToNewAccountBtn"
|
||||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
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]}>
|
<View style={[styles.iconContainer, pal.btn]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="plus"
|
icon="plus"
|
||||||
|
@ -229,7 +240,10 @@ export const SettingsScreen = withAuthRequired(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="inviteFriendBtn"
|
testID="inviteFriendBtn"
|
||||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
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
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.iconContainer,
|
styles.iconContainer,
|
||||||
|
@ -260,7 +274,9 @@ export const SettingsScreen = withAuthRequired(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="contentFilteringBtn"
|
testID="contentFilteringBtn"
|
||||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
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]}>
|
<View style={[styles.iconContainer, pal.btn]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="eye"
|
icon="eye"
|
||||||
|
@ -308,7 +324,10 @@ export const SettingsScreen = withAuthRequired(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="changeHandleBtn"
|
testID="changeHandleBtn"
|
||||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
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]}>
|
<View style={[styles.iconContainer, pal.btn]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="at"
|
icon="at"
|
||||||
|
@ -327,7 +346,11 @@ export const SettingsScreen = withAuthRequired(
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[pal.view, styles.linkCard]}
|
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]}>
|
<View style={[styles.iconContainer, dangerBg]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'trash-can']}
|
icon={['far', 'trash-can']}
|
||||||
|
|
|
@ -56,7 +56,10 @@ export const Composer = observer(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.wrapper, pal.view, wrapperAnimStyle]}>
|
<Animated.View
|
||||||
|
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
|
||||||
|
aria-modal
|
||||||
|
accessibilityViewIsModal>
|
||||||
<ComposePost
|
<ComposePost
|
||||||
replyTo={replyTo}
|
replyTo={replyTo}
|
||||||
onPost={onPost}
|
onPost={onPost}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const Composer = observer(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.mask}>
|
<View style={styles.mask} aria-modal accessibilityViewIsModal>
|
||||||
<View style={[styles.container, pal.view, pal.border]}>
|
<View style={[styles.container, pal.view, pal.border]}>
|
||||||
<ComposePost
|
<ComposePost
|
||||||
replyTo={replyTo}
|
replyTo={replyTo}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, {ComponentProps} from 'react'
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
|
@ -50,6 +50,8 @@ export const DrawerContent = observer(() => {
|
||||||
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
|
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
|
||||||
useNavigationTabState()
|
useNavigationTabState()
|
||||||
|
|
||||||
|
const {notifications} = store.me
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
@ -120,7 +122,11 @@ export const DrawerContent = observer(() => {
|
||||||
]}>
|
]}>
|
||||||
<SafeAreaView style={s.flex1}>
|
<SafeAreaView style={s.flex1}>
|
||||||
<View style={styles.main}>
|
<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} />
|
<UserAvatar size={80} avatar={store.me.avatar} />
|
||||||
<Text
|
<Text
|
||||||
type="title-lg"
|
type="title-lg"
|
||||||
|
@ -164,6 +170,8 @@ export const DrawerContent = observer(() => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
label="Search"
|
label="Search"
|
||||||
|
accessibilityLabel="Search"
|
||||||
|
accessibilityHint="Search through users and posts"
|
||||||
bold={isAtSearch}
|
bold={isAtSearch}
|
||||||
onPress={onPressSearch}
|
onPress={onPressSearch}
|
||||||
/>
|
/>
|
||||||
|
@ -184,6 +192,8 @@ export const DrawerContent = observer(() => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
label="Home"
|
label="Home"
|
||||||
|
accessibilityLabel="Home"
|
||||||
|
accessibilityHint="Navigates to default feed"
|
||||||
bold={isAtHome}
|
bold={isAtHome}
|
||||||
onPress={onPressHome}
|
onPress={onPressHome}
|
||||||
/>
|
/>
|
||||||
|
@ -204,7 +214,13 @@ export const DrawerContent = observer(() => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
label="Notifications"
|
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}
|
bold={isAtNotifications}
|
||||||
onPress={onPressNotifications}
|
onPress={onPressNotifications}
|
||||||
/>
|
/>
|
||||||
|
@ -225,6 +241,8 @@ export const DrawerContent = observer(() => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
label="Profile"
|
label="Profile"
|
||||||
|
accessibilityLabel="Profile"
|
||||||
|
accessibilityHint="See profile display name, avatar, description, and other profile items"
|
||||||
onPress={onPressProfile}
|
onPress={onPressProfile}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -236,6 +254,8 @@ export const DrawerContent = observer(() => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Settings"
|
label="Settings"
|
||||||
|
accessibilityLabel="Settings"
|
||||||
|
accessibilityHint="Manage settings for your account, like handle, content moderation, and app passwords"
|
||||||
onPress={onPressSettings}
|
onPress={onPressSettings}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -243,6 +263,13 @@ export const DrawerContent = observer(() => {
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
{!isWeb && (
|
{!isWeb && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Toggle dark mode"
|
||||||
|
accessibilityHint={
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? 'Sets display to light mode'
|
||||||
|
: 'Sets display to dark mode'
|
||||||
|
}
|
||||||
onPress={onDarkmodePress}
|
onPress={onDarkmodePress}
|
||||||
style={[
|
style={[
|
||||||
styles.footerBtn,
|
styles.footerBtn,
|
||||||
|
@ -258,6 +285,9 @@ export const DrawerContent = observer(() => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="link"
|
||||||
|
accessibilityLabel="Send feedback"
|
||||||
|
accessibilityHint="Opens Google Forms feedback link"
|
||||||
onPress={onPressFeedback}
|
onPress={onPressFeedback}
|
||||||
style={[
|
style={[
|
||||||
styles.footerBtn,
|
styles.footerBtn,
|
||||||
|
@ -281,25 +311,30 @@ export const DrawerContent = observer(() => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function MenuItem({
|
interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> {
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
count,
|
|
||||||
bold,
|
|
||||||
onPress,
|
|
||||||
}: {
|
|
||||||
icon: JSX.Element
|
icon: JSX.Element
|
||||||
label: string
|
label: string
|
||||||
count?: string
|
count?: string
|
||||||
bold?: boolean
|
bold?: boolean
|
||||||
onPress: () => void
|
}
|
||||||
}) {
|
|
||||||
|
function MenuItem({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
accessibilityLabel,
|
||||||
|
count,
|
||||||
|
bold,
|
||||||
|
onPress,
|
||||||
|
}: MenuItemProps) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={`menuItemButton-${label}`}
|
testID={`menuItemButton-${label}`}
|
||||||
style={styles.menuItem}
|
style={styles.menuItem}
|
||||||
onPress={onPress}>
|
onPress={onPress}
|
||||||
|
accessibilityRole="menuitem"
|
||||||
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
accessibilityHint="">
|
||||||
<View style={[styles.menuItemIconWrapper]}>
|
<View style={[styles.menuItemIconWrapper]}>
|
||||||
{icon}
|
{icon}
|
||||||
{count ? (
|
{count ? (
|
||||||
|
@ -332,6 +367,7 @@ const InviteCodes = observer(() => {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {invitesAvailable} = store.me
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
track('Menu:ItemClicked', {url: '#invite-codes'})
|
track('Menu:ItemClicked', {url: '#invite-codes'})
|
||||||
store.shell.closeDrawer()
|
store.shell.closeDrawer()
|
||||||
|
@ -341,7 +377,14 @@ const InviteCodes = observer(() => {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="menuItemInviteCodes"
|
testID="menuItemInviteCodes"
|
||||||
style={[styles.inviteCodes]}
|
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
|
<FontAwesomeIcon
|
||||||
icon="ticket"
|
icon="ticket"
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, {ComponentProps} from 'react'
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
|
@ -94,6 +94,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPress={onPressHome}
|
onPress={onPressHome}
|
||||||
|
accessibilityLabel="Go home"
|
||||||
|
accessibilityHint="Navigates to feed home"
|
||||||
/>
|
/>
|
||||||
<Btn
|
<Btn
|
||||||
testID="bottomBarSearchBtn"
|
testID="bottomBarSearchBtn"
|
||||||
|
@ -113,6 +115,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPress={onPressSearch}
|
onPress={onPressSearch}
|
||||||
|
accessibilityRole="search"
|
||||||
/>
|
/>
|
||||||
<Btn
|
<Btn
|
||||||
testID="bottomBarNotificationsBtn"
|
testID="bottomBarNotificationsBtn"
|
||||||
|
@ -133,6 +136,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||||
}
|
}
|
||||||
onPress={onPressNotifications}
|
onPress={onPressNotifications}
|
||||||
notificationCount={store.me.notifications.unreadCountLabel}
|
notificationCount={store.me.notifications.unreadCountLabel}
|
||||||
|
accessibilityLabel="Notifications"
|
||||||
|
accessibilityHint="Navigates to notifications"
|
||||||
/>
|
/>
|
||||||
<Btn
|
<Btn
|
||||||
testID="bottomBarProfileBtn"
|
testID="bottomBarProfileBtn"
|
||||||
|
@ -154,31 +159,43 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onPress={onPressProfile}
|
onPress={onPressProfile}
|
||||||
|
accessibilityLabel="Profile"
|
||||||
|
accessibilityHint="Navigates to profile"
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</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({
|
function Btn({
|
||||||
testID,
|
testID,
|
||||||
icon,
|
icon,
|
||||||
notificationCount,
|
notificationCount,
|
||||||
onPress,
|
onPress,
|
||||||
onLongPress,
|
onLongPress,
|
||||||
}: {
|
accessibilityHint,
|
||||||
testID?: string
|
accessibilityLabel,
|
||||||
icon: JSX.Element
|
}: BtnProps) {
|
||||||
notificationCount?: string
|
|
||||||
onPress?: (event: GestureResponderEvent) => void
|
|
||||||
onLongPress?: (event: GestureResponderEvent) => void
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={styles.ctrl}
|
style={styles.ctrl}
|
||||||
onPress={onLongPress ? onPress : undefined}
|
onPress={onLongPress ? onPress : undefined}
|
||||||
onPressIn={onLongPress ? undefined : onPress}
|
onPressIn={onLongPress ? undefined : onPress}
|
||||||
onLongPress={onLongPress}>
|
onLongPress={onLongPress}
|
||||||
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
accessibilityHint={accessibilityHint}>
|
||||||
{notificationCount ? (
|
{notificationCount ? (
|
||||||
<View style={[styles.notificationCount]}>
|
<View style={[styles.notificationCount]}>
|
||||||
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
|
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
|
||||||
|
|
|
@ -2,7 +2,11 @@ import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {PressableWithHover} from 'view/com/util/PressableWithHover'
|
import {PressableWithHover} from 'view/com/util/PressableWithHover'
|
||||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
import {
|
||||||
|
useLinkProps,
|
||||||
|
useNavigation,
|
||||||
|
useNavigationState,
|
||||||
|
} from '@react-navigation/native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
|
@ -59,7 +63,10 @@ function BackBtn() {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="viewHeaderBackOrMenuBtn"
|
testID="viewHeaderBackOrMenuBtn"
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
style={styles.backBtn}>
|
style={styles.backBtn}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Go back"
|
||||||
|
accessibilityHint="Navigates to the previous screen">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
size={24}
|
size={24}
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
|
@ -86,25 +93,28 @@ const NavItem = observer(
|
||||||
}
|
}
|
||||||
return getCurrentRoute(state).name
|
return getCurrentRoute(state).name
|
||||||
})
|
})
|
||||||
|
|
||||||
const isCurrent = isTab(currentRouteName, pathName)
|
const isCurrent = isTab(currentRouteName, pathName)
|
||||||
|
const {onPress} = useLinkProps({to: href})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PressableWithHover
|
<PressableWithHover
|
||||||
style={styles.navItemWrapper}
|
style={styles.navItemWrapper}
|
||||||
hoverStyle={pal.viewLight}>
|
hoverStyle={pal.viewLight}
|
||||||
<Link href={href} style={styles.navItem}>
|
onPress={onPress}
|
||||||
<View style={[styles.navItemIconWrapper]}>
|
accessibilityLabel={label}
|
||||||
{isCurrent ? iconFilled : icon}
|
accessibilityHint={`Navigates to ${label}`}>
|
||||||
{typeof count === 'string' && count ? (
|
<View style={[styles.navItemIconWrapper]}>
|
||||||
<Text type="button" style={styles.navItemCount}>
|
{isCurrent ? iconFilled : icon}
|
||||||
{count}
|
{typeof count === 'string' && count ? (
|
||||||
</Text>
|
<Text type="button" style={styles.navItemCount}>
|
||||||
) : null}
|
{count}
|
||||||
</View>
|
</Text>
|
||||||
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
|
) : null}
|
||||||
{label}
|
</View>
|
||||||
</Text>
|
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
|
||||||
</Link>
|
{label}
|
||||||
|
</Text>
|
||||||
</PressableWithHover>
|
</PressableWithHover>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -115,7 +125,12 @@ function ComposeBtn() {
|
||||||
const onPressCompose = () => store.shell.openComposer({})
|
const onPressCompose = () => store.shell.openComposer({})
|
||||||
|
|
||||||
return (
|
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}>
|
<View style={styles.newPostBtnIconWrapper}>
|
||||||
<ComposeIcon2
|
<ComposeIcon2
|
||||||
size={19}
|
size={19}
|
||||||
|
@ -202,7 +217,7 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
profileCard: {
|
profileCard: {
|
||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
width: 60,
|
width: 90,
|
||||||
paddingLeft: 12,
|
paddingLeft: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -215,21 +230,18 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
navItemWrapper: {
|
navItemWrapper: {
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
navItem: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: 12,
|
paddingHorizontal: 12,
|
||||||
paddingBottom: 12,
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
gap: 10,
|
||||||
},
|
},
|
||||||
navItemIconWrapper: {
|
navItemIconWrapper: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
marginRight: 10,
|
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
navItemCount: {
|
navItemCount: {
|
||||||
|
|
|
@ -61,7 +61,14 @@ export const DesktopRightNav = observer(function DesktopRightNav() {
|
||||||
<View>
|
<View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.darkModeToggle]}
|
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]}>
|
<View style={[pal.viewLight, styles.darkModeToggleIcon]}>
|
||||||
<MoonIcon size={18} style={pal.textLight} />
|
<MoonIcon size={18} style={pal.textLight} />
|
||||||
</View>
|
</View>
|
||||||
|
@ -78,13 +85,22 @@ const InviteCodes = observer(() => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
|
const {invitesAvailable} = store.me
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
store.shell.openModal({name: 'invite-codes'})
|
store.shell.openModal({name: 'invite-codes'})
|
||||||
}, [store])
|
}, [store])
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.inviteCodes, pal.border]}
|
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
|
<FontAwesomeIcon
|
||||||
icon="ticket"
|
icon="ticket"
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -67,10 +67,16 @@ export const DesktopSearch = observer(function DesktopSearch() {
|
||||||
onBlur={() => setIsInputFocused(false)}
|
onBlur={() => setIsInputFocused(false)}
|
||||||
onChangeText={onChangeQuery}
|
onChangeText={onChangeQuery}
|
||||||
onSubmitEditing={onSubmit}
|
onSubmitEditing={onSubmit}
|
||||||
|
accessibilityRole="search"
|
||||||
/>
|
/>
|
||||||
{query ? (
|
{query ? (
|
||||||
<View style={styles.cancelBtn}>
|
<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]}>
|
<Text type="lg" style={[pal.link]}>
|
||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -46,7 +46,9 @@ const ShellInner = observer(() => {
|
||||||
{!isDesktop && store.shell.isDrawerOpen && (
|
{!isDesktop && store.shell.isDrawerOpen && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => store.shell.closeDrawer()}
|
onPress={() => store.shell.closeDrawer()}
|
||||||
style={styles.drawerMask}>
|
style={styles.drawerMask}
|
||||||
|
accessibilityLabel="Close navigation footer"
|
||||||
|
accessibilityHint="Closes bottom navigation bar">
|
||||||
<View style={styles.drawerContainer}>
|
<View style={styles.drawerContainer}>
|
||||||
<DrawerContent />
|
<DrawerContent />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -60,10 +60,6 @@
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
/* Remove focus state on inputs */
|
|
||||||
*:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
/* Remove default link styling */
|
/* Remove default link styling */
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
@ -102,6 +98,14 @@
|
||||||
color: #0085ff;
|
color: #0085ff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
/* OLLIE: TODO -- this is not accessible */
|
||||||
|
/* Remove focus state on inputs */
|
||||||
|
.ProseMirror-focused {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
.tippy-content .items {
|
.tippy-content .items {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #F3F3F8;
|
background: #F3F3F8;
|
||||||
|
|
Loading…
Reference in New Issue