-
-
Notifications
You must be signed in to change notification settings - Fork 673
Make the app more accessible to blind people #2753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome -- thanks @borisyankov ! Very glad to see all this. I think accessibility is an important thing for us to stay on top of.
I like the basic direction of all of this. Comments below on specific areas of code style and commit style. I'll go ahead and merge now the bits that I can easily merge as they are; and I think the rest will be easy for you to adjust.
There's one other piece I think would be very helpful to have as part of this effort: a short doc, maybe in docs/architecture/
, about how accessibility in the app works. This really has two audiences:
- Everyone contributing regularly to the app, to be up to speed on how to keep it accessible when making changes and how to fix accessibility issues.
- People interested in using Zulip, for whom accessibility is critical (perhaps because they rely on accessibility features themselves) and who are willing to contribute code, bug reports / user feedback, or other help.
The most day-to-day practical info there might just be a link to the upstream guide:
https://facebook.github.io/react-native/docs/accessibility . No need to duplicate that, though there might be key bits worth highlighting. Other key technical info includes:
- a mention of the webview; this deserves a
docs/architecture/
doc of its own, which I've been meaning to try a draft of, but a brief mention here would be helpful. - maybe a few words about our
Touchable#accessibilityLabel
prop?
Then there's the most important bit of info of all, which is not technical: just saying (to both of those audiences) that this is something we care about, put effort into, and welcome bug reports on. Mostly it's the existence of the doc itself (plus we should link to it appropriately) that communicates that, along with our ongoing response to bug reports; but a sentence or two saying so, plus repeating specifically where to report bugs, will help too.
I'm happy to write up that doc myself if you'd like, or I'd be glad to see a draft in a PR.
src/webview/css/css.js
Outdated
@@ -414,6 +414,13 @@ blockquote { | |||
border-radius: 50%; | |||
background: rgba(82, 194, 175, 0.5); | |||
} | |||
#scroll-bottom .text { | |||
clip: rect(0 0 0 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The web tells me this attribute is deprecated:
https://developer.mozilla.org/en-US/docs/Web/CSS/clip
Hrm, but the suggested replacement isn't yet widely supported:
https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path
Shrug.
src/common/Touchable.js
Outdated
); | ||
} | ||
|
||
if (Platform.OS === 'ios') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to merging these two files -- I think it's actually a win even before the additional logic added in the subsequent commit.
One reason is this deep dark secret 😉 of React Native and Flow, which I noticed recently in our .flowconfig:
[ignore]
; We fork some components by platform
.*/*[.]android.js
(That's verbatim from the template file in RN upstream -- it's not our doing.) In other words, this .android.js
/ .ios.js
convention is totally opaque to Flow, and as a result the best we know how to do is to completely leave .android.js
files out from type-checking.
Similarly, the spiffy navigation features in VS Code, to jump to definition or find references, find this totally opaque.
So there's a pretty big cost to using this .android.js
/.ios.js
feature, as opposed to plain old conditionals.
I think this actually illustrates a kind of classic pattern: once any whiff of the logic of a program starts being expressed in ad-hoc fashion in some kind of configuration outside of the Real Programming Language(s) the rest of the program is written in, it generally isn't long before it begins to hurt to be doing that (a) without the nice, full-featured, composable, semantics of a Real Programming Language and (b) without the benefit of all the tooling that you and your language community have built up around that language (or languages). A very common form of this pattern is templating languages, which either are painfully restrictive or rapidly grow to encompass the main language or both; config-file formats tend to go this way as well.
For us, the Real Programming Language we use for the bulk of the app (including all this code) is JavaScript, and I think plain JS conditionals should express the same thing as the .android.js
/ .ios.js
feature quite nicely. 😁
(I'll go file an issue for that refactor in general, and probably also discuss that general software-systems pattern in chat.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I'll go file an issue for that refactor in general, and probably also discuss that general software-systems pattern in chat.)
(It's late and I think I'm done with focused work for the day, but I've made a note to do this tomorrow.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just ran across this again; filed #3018.
src/common/Touchable.js
Outdated
const androidBackground = | ||
Platform.Version >= 21 | ||
? TouchableNativeFeedback.Ripple(HIGHLIGHT_COLOR) | ||
: TouchableNativeFeedback.SelectableBackground(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these lines definitely OK to run on iOS? They're at top level.
Maybe lines like this are (as a matter of RN API policy) definitely always OK to run on the platform they don't apply to; I don't really know.
I guess you say in the PR message this is tested on iOS, so that's an answer. There's nothing I see in the upstream doc to say what should happen.
src/common/Touchable.js
Outdated
if (!onPress && !onLongPress) { | ||
return ( | ||
<View | ||
accessible={!!accessibilityLabel} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commit message says
By default all 'touchable' components are accessible. But if
we provide an `accessibilityLabel` and render a `View` we set
`accessible` to `true`.
which had me kind of confused for a bit -- by default they're accessible, but we can make them not be? Why would we ever actively make something not accessible? Also, how can something that generic be accessible by default in the first place -- surely that requires more help from the app?
Then the name of this property (on upstream's View
) gave me a hint what you meant, and a bit in this upstream guide:
https://facebook.github.io/react-native/docs/accessibility
explains it: you're referring to this boolean property, which means this (and maybe more things): "When a view is an accessibility element, it groups its children into a single selectable component."
I think the commit message would become almost clear if it just said:
By default all "touchable" components are `accessible`.
so that accessible
is an identifier, not the ordinary word, and "touchable" is in quotes because it's an undefined bit of jargon. Then the message becomes actually clear by including a link to that upstream guide. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also an excellent case for a JSDoc comment, explaining this accessibilityLabel
prop in particular.
src/common/Touchable.js
Outdated
if (!onPress && !onLongPress) { | ||
return ( | ||
<View | ||
accessible={!!accessibilityLabel} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we perhaps always set this to true
here? After all, if onPress
or onLongPress
were set, it'd end up effectively true even without accessibilityLabel
. Also, if the caller thinks of this component as being something touchable, that suggests that it ought to be presented as one thing to select.
Of course ideally we'll make this moot by always providing the label 😄 , but even once we finish doing that there will probably be places we temporarily miss, in the present or the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, now I see your later commit removing uses of nullFunction
.
I don't 100% follow what that commit message is saying, though. Would you write in the identifiers for the components you're mentioning? I'm not quite sure which ones they are.
return text; | ||
} | ||
|
||
return `${text}, ${unreadCount} unread message${unreadCount > 1 ? 's' : ''}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't translate well.
I think https://github.com/yahoo/react-intl/wiki/Components#formattedplural looks like the API we want to use here.
@@ -7,6 +7,7 @@ import { ComponentWithOverlay, UnreadCount } from '../common'; | |||
import Icon from '../common/Icons'; | |||
|
|||
type Props = { | |||
accessibilityLabel: string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this mandatory seems exactly right to me, for the reason you say in the commit message.
I don't like doing so in a commit where the callers don't already comply with that, though -- that's a case of leaving things in a broken state mid-branch.
One very clean way to do this would be to have the first commit add this prop but leave it optional; then add it at all the call sites in subsequent commits, as you do; then have a followup commit just flip it to mandatory.
In general the other clean way to introduce a non-optional prop is to add it at all the call sites in the same commit that introduces it; but in this case there's enough going on that I think it's definitely better to have several separate commits for the call sites, just like you do.
backgroundColor: string, | ||
canGoBack: boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When removing a connect
-supplied prop like this one, please also remove it from the connect
call.
Not only does that leave the code in a tidier end state, it actually makes the diff itself easier to read -- because it causes whatever had been the argument there (in this case, getCanGoBack(state)
) to show up in the diff, making it easier to compare before vs. after. 😃
@@ -357,6 +357,7 @@ class ComposeBox extends PureComponent<Props, State> { | |||
</View> | |||
<View style={styles.alignBottom}> | |||
<FloatingActionButton | |||
accessibilityLabel="Send" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should probably follow the Icon
prop's logic below, right? So when editMessage
is false, it should be "Done" or "Update message" or something.
Also, the new required prop on FloatingActionButton
is a similar story to NavButton
. In this case I think it'd be fine to have a single commit add the prop and adjust all the callers, because that's just a line each in three places; but the cleanest thing would be just like you have, except the first commit leaves it optional and there's an extra commit at the end to make it mandatory.
While splitting code in two files depending on the platfrom is generally a good idea, in the case of Touchable there is too little unique code and too much duplication. [greg: I think this is actually an improvement already, even before the additional code added later! Discussion in #2753.] In anticipation of adding some more code that is exactly the same it is apparent merging them and `if`-ing the differences is better. While some improvements can be made, this refactor keeps the behavior strictly the same but simplifies the logic and makes it more obvious. Instead of WrapperComponent we just check if the component has any onPress or onLongPress event attached, if not returns a `View`
Heads up @borisyankov, we just merged some commits that conflict with the changes your made in this pull request! You can review this repository's recent commits to see where the conflicts occur. Please rebase your feature branch against the |
Merged the core Most of the other commits are also ready to merge, once their dependencies (e.g. |
While splitting code in two files depending on the platfrom is generally a good idea, in the case of Touchable there is too little unique code and too much duplication. [greg: I think this is actually an improvement already, even before the additional code added later! Discussion in zulip#2753.] In anticipation of adding some more code that is exactly the same it is apparent merging them and `if`-ing the differences is better. While some improvements can be made, this refactor keeps the behavior strictly the same but simplifies the logic and makes it more obvious. Instead of WrapperComponent we just check if the component has any onPress or onLongPress event attached, if not returns a `View`
Several components use unread count displays in addition to text describing the item (stream, topic, user etc.) To make adding accessibility labels easier and consistent we create a function (and tests) to create the label.
Including this makes sure the voice assistant does not read the initials in the TextAvatar and also provides the unread count in a human understandable way. Previously it would say: 'JDJD John Doe, Jane Doe, 3' Now: 'John Doe, Jane Doe, 3 unread messages'
Include understandable unread counts
Include unread counts after the topic name.
Use name and unread count
We add `accessibilityLabel` to NavButton, but in contrast with the previously added optional props, this one is mandatory. A NavButton consists only of an icon thus without the label they are completely not-accessible.
This is the same code we use in MainNavBar and ModalNavBar, extracted as a stand-alone and reusable component and also with `accessibilityLabel` set depending on the Platform. Consistent with all other apps: * on iOS the accessibility label is 'Back button' * on Android the label is 'Navigate up button'
Replace the customized NavButton in MainNavBar with the better BackButton for reusability and added accessibility.
Replace customized NavButton with the accessible BackButton.
Replace the slightly outdated NavBar plus logic code with the accessible BackButton component.
Now that NavBar requires an `accessibilityLabel` property, it is obvious what components need to have this added. Previously these components were invisible to the voice-over functionality, now they have a meaningful text associated with them.
Any `Touchable` is accessible by default. Any `Touchable` that does not have an `onPress` though is rendered as `View` which can be accessible but is not so automatically. We do not want all avatars to be accessible. To the average user an avatar with onPress of `nullFunction` is exactly the same as one with none. To a person using voice-over there is a difference. By removing the default onPress value entirely we make sure these components are not accessible by default. Thus many elements that were misleading or 'fake accessible' are now ignored (meaning that the voice-over function will try to read them but we don't want to) Such components are: * PM titlebar's avatar * Group titlebar avatars * any other unanticipaded component
The button consist of a single icon, thus was not accessible. We add `accessibilityLabel` prop that is passed to the touchable component inside.
This is the most important UI element remaining inaccessible. The buttons are currently icon-only and need accessibilityLabel. In addition to that, the IconUnreadConversations is enhanced with a readable (hearable) unread counts.
@gnprice would it be possible to get some priority on this PR? Our organization currently has a visually impaired user who is unable to use our main communication application on his iphone. If passes are checking, could we please merge this so that the application is more accessible? Also here is the issue I created a while back about this problem #4232 |
@jesse-troy Thanks for the ping. Let's discuss on the issue thread (or chat.zulip.org) -- I've just added a comment there. |
#4419 is open, which may mean it'll soon be time to close this PR. We'll want to do at least a quick check that there isn't anything else here in Boris's PR that we want before closing it. Greg said, at #4394 (comment):
|
We already follow standard usability best-practices like having big enough touch targets.
This PR improves the usability for blind people.
It was tested with
TalkBack
on Android andVoiceOver
on iOS.It consists of separate changes that ensure UI elements are identified correctly and their purpose can be determined (usually by adding a descriptive text)