Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Conversation

taras-yemets
Copy link
Contributor

@taras-yemets taras-yemets commented Nov 9, 2022

Closes https://github.com/sourcegraph/sourcegraph/issues/43211

Moves blame decorations from gutter to the code line.
Two-dimensional code navigation suggestion suggested in this comment will be addressed in a follow-up issue https://github.com/sourcegraph/sourcegraph/issues/44594.

Screen.Recording.2022-11-18.at.11.04.17.mov

⚠️ Implementation details

  1. Blame decorations were moved from a separate div inside the line with the code and no longer had full-height background. I tried to achieve full-height-like view of inline decorations with CSS only by playing around with spacing and pseudo elements (what we do with the default blob viewer but with ::before and ::after), but it neither approach didn't work: spacing increases code line hight and it looks weird when selected/highlighted and pseudo elements are removed by the CodeMirror editor.
    To achieve the full-height-like column view with inline blame decorations we render empty gutter providing with and background color and shift the line content to the left by var(--blame-decoration-width) so that inline blame decorations make use of the full-height background provided by the gutter.
    This approach comes with a drawback: we have to disable gutters sticky position (including line numbers) so that the blame gutter background is not scrolled horizontally with the content while the inline blame decoration is not.
  2. The suggested approach improves git blame decorations readability (they become accessible by the screenreader and can be read line by line along with the code they reference to). But it looks a bit hacky as we still use two different approaches (inline decorations and gutter) to achieve this effect and it may be not the way the CodeMirror authors expect us to use the editor. CodeMirror on its website claims that it "works well with screen readers and keyboard-only users" so I'll browse their forum to find similar use-cases or post an issue to get some guidance on how to make gutters keyboard and screenreader accessible. If there's a better approach we'll address it in a follow-up PR. cc: @fkling

Previous description 👇🏻 (didn't remove as it's referenced in comments, please ignore when reviewing the suggested changes)
>> start
Removes aria-hidden="false" from the CodeMirror editor gutters.

Although it fixes the referenced issue, I'm not sure it is the best approach to the CodeMirror file view with blame decorations open readability.
When the screen reader reaches the code editor area, it reads the content in the following order (see screenshot):

  1. all the line numbers one by one
  2. all the blame decorations one by one
  3. code content.

Screenshot 2022-11-09 at 15 44 16

And it doesn't look like a perfect user experience 😞

Although we can hide line numbers from the screen reader (but do we really want it: Monaco editor and GitHub blame view, for example, announce line numbers), it doesn't help with reading all the blame decorations one by one before reaching the file content.

Potential solution
Line numbers and other types of content CodeMirror users may want to put into gutters (gutter description) are not expected to be accessible by the CodeMirror creators (source).
The potential solution could be to move blame decorations (and maybe line numbers) inside the line content (e.g., prepend the line code content with line number and commit info - see screenshot) so that they are announced in a more consumable way (e.g., "line 1", "$COMMIT_INFO", "$LINE_CONTENT"). But I'm not sure if it's a feasible way as I'm not a power CodeMirror user (I hope @fkling can help here).

Screenshot 2022-11-09 at 16 09 43

>> end

Test plan

Tested manually that the git blame column content is read by the screenreader (VoiceOver).

App preview:

Check out the client app preview documentation to learn more.

@cla-bot cla-bot bot added the cla-signed label Nov 9, 2022
@sg-e2e-regression-test-bob
Copy link

sg-e2e-regression-test-bob commented Nov 9, 2022

Bundle size report 📦

Initial size Total size Async size Modules
0.00% (0.00 kb) 0.01% (+0.75 kb) 0.01% (+0.75 kb) 0.00% (0)

Look at the Statoscope report for a full comparison between the commits 22bd1b1 and 5ee629d or learn more.

Open explanation
  • Initial size is the size of the initial bundle (the one that is loaded when you open the page)
  • Total size is the size of the initial bundle + all the async loaded chunks
  • Async size is the size of all the async loaded chunks
  • Modules is the number of modules in the initial bundle

@taras-yemets taras-yemets marked this pull request as ready for review November 9, 2022 14:22
@philipp-spiess
Copy link
Contributor

philipp-spiess commented Nov 9, 2022

Thanks for the writeup @taras-yemets.

The potential solution could be to move blame decorations (and maybe line numbers) inside the line content (e.g., prepend the line code content with line number and commit info - see screenshot) so that they are announced in a more consumable way (e.g., "line 1", "$COMMIT_INFO", "$LINE_CONTENT"). But I'm not sure if it's a feasible way as I'm not a power CodeMirror user (I hope @fkling can help here).

Yeah if this would be possible this would be the best solution.


I think another problem with announcing line numbers with this DOM structure is that the screenreader would first read all line numbers one by one (1, 2, 3, ...) before it even gets to the code part. So this does not seem very useful :/ So maybe the line numbers should stay aria-hidden="true"?

The disconnect between the git blame annotation and the line of code is also annoying but maybe we can add a screenreader-only line number in here (so we only announce line numbers if we render git blame). This way there is at least some connection between the git blame information and the line of code? E.g.:

  <div class="cm-gutter BlameColumn">
      <div class="BlameRow">
+       <div class="sr_only">123</div>
        ...the rest...
       </div>
     </div>
  </div>

@fkling
Copy link
Contributor

fkling commented Nov 9, 2022

How is the code read? Line by line? It would be possible to insert a screenreader only widget decoration at the start of each line.

// Make gutters content visible for the screen readers
useEffect(() => {
if (editor) {
editor.dom.querySelector('.cm-gutters')?.removeAttribute('aria-hidden')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not work anyways because the DOM is controlled by CodeMirror (so the attribute might be set again at some point).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah our other consideration was to use something like patch-package. The default here is really bad since it makes none of the gutters accessible for screenreaders, so maybe there's an argument for fixing it upstream (and only apply aria-hidden to the line numbers) 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not work anyways because the DOM is controlled by CodeMirror (so the attribute might be set again at some point).

What if we add gutters' dependencies (file blob, blame hunks, etc.) as useEffect dependencies? Could it help?
But yeah, it still sounds unsafe.

@philipp-spiess
Copy link
Contributor

philipp-spiess commented Nov 9, 2022

How is the code read? Line by line?

Yep:

Screenshot 2022-11-09 at 17 36 39

Edit: oh looks like the text contents changed when I made the screenshot but it was saying that I’m on a text node and I could navigate through the rows with ctrl+option+arrow right

It would be possible to insert a screenreader only widget decoration at the start of each line.

Would it be possible to also put links into a screenreader only widget? In that case, is the idea that render the git blame view as-is with aria-hidden="" and have a separate implementation that is only for screenreaders?

@taras-yemets
Copy link
Contributor Author

taras-yemets commented Nov 9, 2022

@philipp-spiess

I could navigate through the rows with ctrl+option+arrow right

Yes, but only on inside code rows; blame decorations don't seem to be available this way.

Would it be possible to also put links into a screenreader-only widget?

I can hardly imagine a hidden clickable element. Maybe I'm missing some context here...

@felixfbecker
Copy link
Contributor

felixfbecker commented Nov 9, 2022

Anything can be put into an sr-only element, it's simply moved off-screen. Which unfortunately also makes it a bit hacky and a last-resort solution, because the VoiceOver cursor will be in awkward places instead of over the corresponding visual elements.

@felixfbecker
Copy link
Contributor

felixfbecker commented Nov 9, 2022

It seems like a really bad issue for CodeMirror that Gutter decorations like blame and line numbers are completely inaccessible. GitHub's code view and our old blob view expose these. We should definitely at least report it upstream, I couldn't find any open issue from a quick search...

@felixfbecker
Copy link
Contributor

On second thought, it's understandable why they do this. They want to use contenteditable for editing functionality. Obviously you wouldn't want the line numbers to be part of the editable area, so they are distinct subtrees of the DOM. So there isn't an easy solution I can think of that we could contribute upstream 🤔

You really want the line numbers, gutter etc to be associated with each other somehow. We will have this problem with the diff view too, if we want to migrate to CodeMirror there – you want the rows of the left and right-hand side of the diff to be associated with each other to enable side-by-side and up-and-down traversal. That's why we used a <table> before (the diff view still does) and GitHub does too.

For line numbers, maybe the "association" could be achieved with aria-describedby, but for interactive content like blame and diff view it would get tricky.

The only idea I have is if CodeMirror used a table too, and maybe it would be possible to set contenteditable="false" on each line number and gutter cell, and add some JS handling to make sure inserts a new line as a new row and not within the cell (they must have some kind of handling like this already for updating line numbers). I don't know if this would work though, and in the context of needing to support editing in CodeMirror, it would probably be quite a big change for us to make just to get good read-only accessibility support.

@felixfbecker felixfbecker added accessibility a11y / accessibility wcag/2.1 labels Nov 10, 2022
@fkling
Copy link
Contributor

fkling commented Nov 10, 2022

A couple of thoughts:

  • It's possible to add "line decorations" to all (visible) lines and set arbitrary attributes on them (afaik). This could allow us to add the line number as aria-* attribute one way or another.
  • How do screenreader users even want to consume additional information like git blame? I'm not a screenreader user so I can't answer that question, but one possibility would be to somehow associate keyboard bindings with aria announcements. Maybe having some kind of "menu" for getting more information is more useful? E.g. pressing gb could read the blame information for the current line.
    More generally, maybe we need to design an overall different (better?) experience for screenreader users.
  • I'll do some tests for my own understanding how CodeMirror works with screenreader and depending on those results I'll post on the discussion board about the issues raised here (e.g. line numbers). A11y is important for CodeMirror (for all I know). Even if some of these things cannot be changed on the CodeMirror side, Marijn might have suggestions for how to address these issues.

@philipp-spiess
Copy link
Contributor

philipp-spiess commented Nov 10, 2022

Taras and I were looking into using widget decorations for this earlier today: We inserted a span at the beginning of every visible line to add a link (to mimic the git blame column).

We got pretty far with that approach: The blame annotation is read by the screenreader as part of every line and it is automatically added as contenteditable="false". It also rendered faster: Maybe this is because of how we initialize the react tree inside a gutter column but we noticed that when you scroll down, the widget decoration is rendered instantly while the git blame gutter is loaded lazily and only appears after a few hundred ms.

The screenreader would then read the blame information at the beginning of every line, so this approach mimics GitHub's blame view in that regard.

So one option we can consider now is to move all of the git blame column inside the row as a widget: It doesn't even have to be sr_only, we can probably get it to look the same as the current gutter and we can easily add the hover overlays as well. The tab order will be the same as it is now until we make symbols inside the code tab-navigateable.

How do screenreader users even want to consume additional information like git blame?

Yeah that is the big question. How can we find out an answer to that? I looked at a few other git blame views across the web: including GitHubs blame view, VS Code with GitLens, and Google Code Search:

GitHub: Reads the blame information for each row. Most notably if a blame hunk is shown for multiple lines, it will only read it before the first line of this hunk. This is the order that the screenreader is reading:

github.order.mov

VSCode with GitLens: Does not read the git blame data at all

Google Code Search: They had clearly the worst experience, the screenreader was first reading all line numbers "1, 2, 3, ..." then all the source code and then all the blame rows 🤷

From what I learned by this exercise it's very safe for me to say that GitHub was the only thing remotely useful.

The only idea I have is if CodeMirror used a table too

I don't see any way of doing this without forking CodeMirror, but maybe I’m missing something?

@philipp-spiess
Copy link
Contributor

^ To add to the examples above:

GitLab: Similar to GitHub but slightly worse: The screenreader starts with the blame information, then reads all line numbers for the affect hunk, followed by the code:

gitlab.mov

@philipp-spiess
Copy link
Contributor

I noticed another minor improvement that moving the git blame rendering inside a line could bring: Right now, when the viewport width is constrained, we resort to scrolling of the code view. If the git blame gutter is shown, it will take up a lot of space and make the scrollable area very small:

Screenshot 2022-11-10 at 17 06 38

@taras-yemets
Copy link
Contributor Author

A couple of findings 👇🏻

  • Despite having aria-hidden="true" on the gutters wrapper, blame decorations are announced by the screen reader. Here's the video recorder on the main branch.
Screen.Recording.2022-11-10.at.18.54.32.mov
  • Adding sr-only blame decorations to the beginning of the code line (as suggested here) helps with the order of reading: blame decoration content is announced before the line (code) content, but it has two drawbacks in my opinion:
    • Despite being sr-only 'hidden,' blame decoration is still focusable, and it results in having two popups. I think there is a way to tweak this behavior, but I'm not sure if it's worth the effort considering the next point.
    • Having blame decoration text right before the line code content doesn't make the content more comprehensive, in my opinion. Blame decoration content and code content are announced as one big chunk of text (with mentioning blame decoration being a link, of course), and I can hardly understand the actual meaning.
Screen.Recording.2022-11-10.at.19.09.43.mov
  • Moving blame decorations content from the gutter to widget decorations helps with the order of reading but still has the same comprehension problems as described in the previous point, in my opinion.
    Please ignore the styling problems in the video below; it's just for demo purposes.
Screen.Recording.2022-11-10.at.19.16.43.mov

After trying a bunch of different approaches, it seems to me that neither of them improves the screen reader's user experience dramatically, so I'm not sure if we should invest in the future development of any of them until we have a good understanding of what the ideal screen reader user journey is. Just to announce all the text content (line numbers, blame decorations, code content) without distinguishing it in a meaningful way seems insufficient (GitHub, GitLab, VSCode, and Google Code Search examples gathered by @philipp-spiess prove that, in my opinion).
As of now, we have blame decorations announced as aria-hidden="true" is ignored. Given that, do we still consider https://github.com/sourcegraph/sourcegraph/issues/43211 as critical for the upcoming audit?

cc: @philipp-spiess, @felixfbecker, @fkling

@philipp-spiess
Copy link
Contributor

philipp-spiess commented Nov 10, 2022

Despite being aria 'hidden,' blame decoration is still focusable, and it results in having two popups. I think there is a way to tweak this behavior, but I'm not sure if it's worth the effort considering the next point.

I can only reproduce this with tab navigation, not with ctrl+option+arrow right. So in the normal reading order, this is not true I believe. I might be missing something though!

More on that: https://accessibilityinsights.io/info-examples/web/aria-hidden-focus/

Having blame decoration text right before the line code content doesn't make the content more comprehensive, in my opinion. Blame decoration content and code content are announced as one big chunk of text (with mentioning blame decoration being a link, of course), and I can hardly understand the actual meaning.

Yeah one thing I noticed at both GitHub and GitLab is that they announce the line number between the blame text and the code text. I think we could do this to (sr_only). This could make up for the aria-hidden on the line number gutter.

@philipp-spiess
Copy link
Contributor

Having blame decoration text right before the line code content doesn't make the content more comprehensive, in my opinion. Blame decoration content and code content are announced as one big chunk of text (with mentioning blame decoration being a link, of course), and I can hardly understand the actual meaning.

I want to add here that It's probably hard for us to judge what is comprehensive and what not with how little experience (at least I) have with screen readers. There is still a difference though between connecting the blame information with the code line vs. not doing that. I would err on the side of giving more data (in this case the connection to the line of code) over making the information inaccessible.

If this is distracting for users, they can always disable git blame anyways.

@taras-yemets
Copy link
Contributor Author

I can only reproduce this with tab navigation, not with ctrl+option+arrow right. So in the normal reading order, this is not true I believe. I might be missing something though!
More on that: https://accessibilityinsights.io/info-examples/web/aria-hidden-focus/

I totally missed that! Thank you, @philipp-spiess!

@taras-yemets taras-yemets requested a review from a team November 18, 2022 09:12
@taras-yemets taras-yemets marked this pull request as ready for review November 18, 2022 09:12
@taras-yemets taras-yemets requested a review from fkling November 18, 2022 09:37
@philipp-spiess
Copy link
Contributor

@taras-yemets Thanks for putting the PR up for review!

I was checking it out locally and noticed a strange behavior while scrolling:

There seems to be a delay between the git blame being rendered and the code being rendered. In that the, the text is shifted and rendered where the git blame should be.

It also felt awkward when I first opened git blame and there was no UI feedback until the git blame data appeared from the backend.

Is there any chance we can always render at least an empty placeholder? 🤔 Or maybe at least make it so that the text always starts offset so that there's no layout shift?

Screen.Recording.2022-11-18.at.11.20.47.mov

@taras-yemets
Copy link
Contributor Author

There seems to be a delay between the git blame being rendered and the code being rendered. In that the, the text is shifted and rendered where the git blame should be.

@philipp-spiess, nice catch! Thank you! On it.

@taras-yemets taras-yemets marked this pull request as draft November 18, 2022 11:01
@taras-yemets
Copy link
Contributor Author

I was checking it out locally and noticed a strange behavior while scrolling:
There seems to be a delay between the git blame being rendered and the code being rendered. In that the, the text is shifted and rendered where the git blame should be.
It also felt awkward when I first opened git blame and there was no UI feedback until the git blame data appeared from the backend.
Is there any chance we can always render at least an empty placeholder? 🤔 Or maybe at least make it so that the text always starts offset so that there's no layout shift?

@philipp-spiess, addressed by rendering placeholders while waiting for the actual blame data.

blame.big.file.mov

@taras-yemets taras-yemets marked this pull request as ready for review November 22, 2022 16:11
@felixfbecker felixfbecker changed the title [Accessibility]: make CodeMirror gutters readable by the screenreader [Accessibility]: move blame info from gutter into CodeMirror decorations to make them readable by screenreaders Nov 22, 2022
Copy link
Contributor

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Obviously this is very hacky but the benefits are great and it makes the content accessible.

I posted some thoughts for future improvements but for now this looks good (I also played around with it locally and couldn't tell the difference)

* so that they can be used as gutter spacer.
*/
const longestColumnDecorations = (hunks?: BlameHunk[]): BlameHunk | undefined =>
hunks?.reduce((acc, hunk) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phew good that we got rid of this, this requires traversal through all hunks 😅

for (const { from, to } of view.visibleRanges) {
for (let position = from; position <= to; ) {
const line = view.state.doc.lineAt(position)
const matchingHunk = hunks.find(hunk => hunk.startLine === line.number)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we convert hunk into a Map so we have faster lookup here in a follow-up?

There's a catch though: With #44287 we'll need to be able to know what the hunk is for every line, not just the start line, so we can render the proper recency color.

So we'll need to do something like:

const map = new Map<number, Hunk>();
for (const hunk of hunks) {
  for (const line = hunk.startLine; line <= hunk.endLine; line++) {
    if (map.has(line)) {
      console.error(
        "This shouldn't happen, backend sent overlapping blame hunks"
      );
    }
    map.set(line, hunk);
  }
}

Let's wait for this change though until we also merge #44199 which changes a lot on that infra.

@taras-yemets taras-yemets merged commit 12be752 into main Nov 23, 2022
@taras-yemets taras-yemets deleted the ps/make-codemirror-gutters-readable-by-screenreaders branch November 23, 2022 17:11
@felixfbecker
Copy link
Contributor

Obviously this is very hacky but the benefits are great

@philipp-spiess could you elaborate the "obviously hacky" aspects and maybe file a follow-up issue to fix those? Since the whole CodeMirror migration is supposed to be a better more flexible architecture, we should of course strive to not have core features like this implemented in an "obviously hacky" way

@philipp-spiess
Copy link
Contributor

philipp-spiess commented Nov 28, 2022

@felixfbecker I was replying to the updated message in the PR description, to quote @taras-yemets:

But it looks a bit hacky as we still use two different approaches (inline decorations and gutter) to achieve this effect and it may be not the way the CodeMirror authors expect us to use the editor. CodeMirror on its website claims that it "works well with screen readers and keyboard-only users" so I'll browse their forum to find similar use-cases or post an issue to get some guidance on how to make gutters keyboard and screenreader accessible. If there's a better approach we'll address it in a follow-up PR. cc: @fkling

I think the follow-up that Taras proposed sounds good as well. I think this is something that should be fixed upstream and maybe we can even help with that.

Sorry if "obviously" came a bit too harsh. This change is a great improvement to our accessibility so it's definitely worth it, even if we had to bend the rules a bit.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Git blame column is completely inaccessible (aria-hidden)
5 participants