mdx: In-page hash navigation does not set focus

I have initially logged this as an issue on the gatsby-mdx repo here;

Describe the bug When I click an anchor that refers to a location inside the page, the destination element does not receive proper focus. Although the browser scroll position is reset properly, the destination element is not read out by screen readers as it does not receive focus correctly.

To Reproduce

  1. Clone repository https://github.com/AlmeroSteyn/gatsby-mdx-minimal-repro
  2. Go to homepage
  3. Click on the link right at the top named Skip to content
  4. Note how the container below with the text This is some content does not get a focus outline.
  5. Now go to route /focus which is a straight React component without MDX.
  6. Click on link right at the top named Skip to content
  7. Note how the container below with text This is some content DOES get a focus outline on this page.

Both of these pages uses te same Layout component that contains the link and in-page navigation destination. When used in an MDX file it doesn’t work, while using it as a straight page component does work

When I was looking into why this was happening I noticed that on the MDX pages the entire content is remounted even when the hash portion of the URL changes. This breaks the standard browser behaviour of setting focus to the target element.

Because focus is not set correctly, screen readers will not announce to the users that any action has been performed.

During triage of the issue on the gatsby-mdx repo it was found that setting a shouldComponentUpdate flag based on the location.hash value on the MDXTag component will fix the issue in some cases. However the following cases should work and properly set focus to the element in question:

  1. Clicking a hash anchor when the browser URL is not set to the hash location.
  2. Clicking a hash anchor when the browser URL was set before by a previous click.
  3. Refreshing a page when the browser URL already contains the hash.

Expected behavior Navigating to an element with an id corresponding to the hash in the href of the anchor should set focus on the element to allow screen reader users to also use applications built with MDX.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 32 (18 by maintainers)

Commits related to this issue

Most upvoted comments

@silvenon I’d like that yes! Next week sometime?

I tried to put the PR together but getting it to commit is taking more time than I have right now. So sorry. I’m working on a Windows environment and things like the format script is failing for me. Also having line ending hell as all LF gets changed to CRLF when Prettier runs and then it wants to commit all files.

I’m terribly sorry to hear that. When you have time I’d like to pair with you to solve the development environment, because the point of that setup was to minimize the friction of enforcing a code style.

Thanks for writing the docs here, your great! I’ll set you as the author when I’m committing the changes, so it should still show up as your contribution.

@silvenon I tried to put the PR together but getting it to commit is taking more time than I have right now. So sorry. I’m working on a Windows environment and things like the format script is failing for me. Also having line ending hell as all LF gets changed to CRLF when Prettier runs and then it wants to commit all files.

I’d get there eventually but gotta turn attention to other things here right now so would it be OK if I give you the text I was going to add to the Getting Started doc so you can add it?

Here it is:

How it works

MDXProvider uses [React Context][] to provide the component mapping to MDXTag. This way MDXTag knows that it should override the standard components with those provided in this mapping.

Important usage intructions

Because MDXProvider uses React Context directly, it is affected by the same caveats.

It is therefore important that you DON’T declare your components mapping inline in the JSX. Doing so will trigger a rerender of your entire MDX page with every render cycle. Not only is this bad for performance, but it can cause unwanted side affects, like breaking in-page browser navigation.

Avoid this by following the pattern shown above and declare your mapping as a constant.

Updating the mapping object during application runtime

If you need to change the mapping during runtime, declare it on the component’s state object:

import React from 'react'
import { MDXProvider } from '@mdx-js/tag'

import { Heading, Text, Pre, Code, Table } from './components'

export default class Layout extends React.Component {
  state = {
    h1: Heading.H1,
    h2: Heading.H2,
    // ...
    p: Text,
    code: Pre,
    inlineCode: Code
  };
  
  render(){
    const {children, ...props} = this.props;
    return <MDXProvider components={this.state}>
        <main {...props} >
          {children}
          </main>
    </MDXProvider>
  }
}

You can now use the standard setState function to update the mapping object and be assured that it won’t trigger unnecessary renders.

Can probably be fixed with a memoized function deep checking the components prop.

Can it account for potentially new function references too if a given function is defined inline? ala

<MDXProvider components={{
  h1: ({children, ...props}) => <h1>{children}</h1>
}}>

nicely done on the digging btw, I think that may be the last piece to get some MDX react-pose staggering I was working on.

No worries! I understand how it goes and it is an awesome tool!

I got a workaround in place for now by manually setting focus inside the Layout component so not dead in the water.

@silvenon Thank you! Will check this out first thing tomorrow. This fix will really help screen reader users a lot.

@ChristopherBiscardi This is great news! Thanks for jumping on this. Looking forward to the release.

@AlmeroSteyn I’ve confirmed that the above layout change works in your repro repo. Please try it and let me know.

@johno I don’t think we need keys (or any other changes I was making) anywhere but this does seem like an awkward footgun in mdx that should be documented at least.