From 83959c595d52ceb7aa4e3f68441c5ac41c389ebc Mon Sep 17 00:00:00 2001
From: Ollie H <renahlee@outlook.com>
Date: Mon, 1 May 2023 18:38:47 -0700
Subject: 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>
---
 src/view/com/composer/Composer.tsx | 274 ++++++++++++++++++-------------------
 1 file changed, 135 insertions(+), 139 deletions(-)

(limited to 'src/view/com/composer/Composer.tsx')

diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 5ccc229d6..45e67d7cb 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -7,7 +7,6 @@ import {
   ScrollView,
   StyleSheet,
   TouchableOpacity,
-  TouchableWithoutFeedback,
   View,
 } from 'react-native'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -19,6 +18,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {ExternalEmbed} from './ExternalEmbed'
 import {Text} from '../util/text/Text'
 import * as Toast from '../util/Toast'
+// TODO: Prevent naming components that coincide with RN primitives
+// due to linting false positives
 import {TextInput, TextInputRef} from './text-input/TextInput'
 import {CharProgress} from './char-progress/CharProgress'
 import {UserAvatar} from '../util/UserAvatar'
@@ -87,27 +88,6 @@ export const ComposePost = observer(function ComposePost({
     autocompleteView.setup()
   }, [autocompleteView])
 
-  useEffect(() => {
-    // HACK
-    // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
-    // -prf
-    let to: NodeJS.Timeout | undefined
-    if (textInput.current) {
-      to = setTimeout(() => {
-        textInput.current?.focus()
-      }, 250)
-    }
-    return () => {
-      if (to) {
-        clearTimeout(to)
-      }
-    }
-  }, [])
-
-  const onPressContainer = useCallback(() => {
-    textInput.current?.focus()
-  }, [textInput])
-
   const onPressAddLinkCard = useCallback(
     (uri: string) => {
       setExtLink({uri, isLoading: true})
@@ -133,7 +113,7 @@ export const ComposePost = observer(function ComposePost({
 
       if (rt.text.trim().length === 0 && gallery.isEmpty) {
         setError('Did you want to say anything?')
-        return false
+        return
       }
 
       setIsProcessing(true)
@@ -203,133 +183,149 @@ export const ComposePost = observer(function ComposePost({
       testID="composePostView"
       behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
       style={styles.outer}>
-      <TouchableWithoutFeedback onPressIn={onPressContainer}>
-        <View style={[s.flex1, viewStyles]}>
-          <View style={styles.topbar}>
+      <View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
+        <View style={styles.topbar}>
+          <TouchableOpacity
+            testID="composerCancelButton"
+            onPress={hackfixOnClose}
+            onAccessibilityEscape={hackfixOnClose}
+            accessibilityRole="button"
+            accessibilityLabel="Cancel"
+            accessibilityHint="Closes post composer">
+            <Text style={[pal.link, s.f18]}>Cancel</Text>
+          </TouchableOpacity>
+          <View style={s.flex1} />
+          {isProcessing ? (
+            <View style={styles.postBtn}>
+              <ActivityIndicator />
+            </View>
+          ) : canPost ? (
             <TouchableOpacity
-              testID="composerCancelButton"
-              onPress={hackfixOnClose}>
-              <Text style={[pal.link, s.f18]}>Cancel</Text>
+              testID="composerPublishBtn"
+              onPress={() => {
+                onPressPublish(richtext)
+              }}
+              accessibilityRole="button"
+              accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'}
+              accessibilityHint={
+                replyTo
+                  ? 'Double tap to publish your reply'
+                  : 'Double tap to publish your post'
+              }>
+              <LinearGradient
+                colors={[gradients.blueLight.start, gradients.blueLight.end]}
+                start={{x: 0, y: 0}}
+                end={{x: 1, y: 1}}
+                style={styles.postBtn}>
+                <Text style={[s.white, s.f16, s.bold]}>
+                  {replyTo ? 'Reply' : 'Post'}
+                </Text>
+              </LinearGradient>
             </TouchableOpacity>
-            <View style={s.flex1} />
-            {isProcessing ? (
-              <View style={styles.postBtn}>
-                <ActivityIndicator />
-              </View>
-            ) : canPost ? (
-              <TouchableOpacity
-                testID="composerPublishBtn"
-                onPress={() => {
-                  onPressPublish(richtext)
-                }}>
-                <LinearGradient
-                  colors={[gradients.blueLight.start, gradients.blueLight.end]}
-                  start={{x: 0, y: 0}}
-                  end={{x: 1, y: 1}}
-                  style={styles.postBtn}>
-                  <Text style={[s.white, s.f16, s.bold]}>
-                    {replyTo ? 'Reply' : 'Post'}
-                  </Text>
-                </LinearGradient>
-              </TouchableOpacity>
-            ) : (
-              <View style={[styles.postBtn, pal.btn]}>
-                <Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
-              </View>
-            )}
+          ) : (
+            <View style={[styles.postBtn, pal.btn]}>
+              <Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
+            </View>
+          )}
+        </View>
+        {isProcessing ? (
+          <View style={[pal.btn, styles.processingLine]}>
+            <Text style={pal.text}>{processingState}</Text>
           </View>
-          {isProcessing ? (
-            <View style={[pal.btn, styles.processingLine]}>
-              <Text style={pal.text}>{processingState}</Text>
+        ) : undefined}
+        {error !== '' && (
+          <View style={styles.errorLine}>
+            <View style={styles.errorIcon}>
+              <FontAwesomeIcon
+                icon="exclamation"
+                style={{color: colors.red4}}
+                size={10}
+              />
             </View>
-          ) : undefined}
-          {error !== '' && (
-            <View style={styles.errorLine}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon
-                  icon="exclamation"
-                  style={{color: colors.red4}}
-                  size={10}
-                />
+            <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>
-              <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}
+          ) : 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>
+          <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)}>
-                  <Text style={pal.text}>
-                    Add link card: <Text style={pal.link}>{url}</Text>
-                  </Text>
-                </TouchableOpacity>
-              ))}
+          <Gallery gallery={gallery} />
+          {gallery.isEmpty && extLink && (
+            <ExternalEmbed
+              link={extLink}
+              onRemove={() => setExtLink(undefined)}
+            />
+          )}
+          {quote ? (
+            <View style={s.mt5}>
+              <QuoteEmbed quote={quote} />
             </View>
-          ) : null}
-          <View style={[pal.border, styles.bottomBar]}>
-            {canSelectImages ? (
-              <>
-                <SelectPhotoBtn gallery={gallery} />
-                <OpenCameraBtn gallery={gallery} />
-              </>
-            ) : null}
-            <View style={s.flex1} />
-            <CharProgress count={graphemeLength} />
+          ) : 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>
-      </TouchableWithoutFeedback>
+      </View>
     </KeyboardAvoidingView>
   )
 })
-- 
cgit 1.4.1

