Skip to content
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

(Accessibility) Implement client routing solution for all products #135760

Closed
2 tasks done
1Copenut opened this issue Jul 5, 2022 · 21 comments
Closed
2 tasks done

(Accessibility) Implement client routing solution for all products #135760

1Copenut opened this issue Jul 5, 2022 · 21 comments
Assignees
Labels
EUI Meta Project:Accessibility Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:SharedUX Team label for AppEx-SharedUX (formerly Global Experience) WCAG A

Comments

@1Copenut
Copy link
Contributor

1Copenut commented Jul 5, 2022

Description
The EUI team has been working on a long-term solution for a universal client-side routing announcement for screen readers. We have adopted this solution into the EUI Docs and are now wanting to bring it to Kibana products.

This meta issue will be a way to track implementation work for products within Kibana.

Tracking issue: #42379

Acceptance criteria

  • New pages or routes are announced correctly to screen readers
  • The EuiScreenReaderLive is ideally the first thing in the React DOM source order, so
  • The EuiSkipLink can be the second element in the React DOM source order

We're trying to smooth out potential bumps in the road by settling on a pattern that adds a new component as the very first component in the div#root container (or whatever your team is declaring as the React root container). This means the skip link can be added or moved to be the second component, and the rest of your application structure shouldn't need to be adjusted.

Observability

Relevant WCAG Criteria:

@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-accessibility (Project:Accessibility)

@botelastic botelastic bot added the needs-team Issues missing a team label label Jul 5, 2022
@drewdaemon drewdaemon added the Team:Infra Monitoring UI - DEPRECATED DEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_services label Jul 7, 2022
@elasticmachine
Copy link
Contributor

Pinging @elastic/infra-monitoring-ui (Team:Infra Monitoring UI)

@botelastic botelastic bot removed the needs-team Issues missing a team label label Jul 7, 2022
@smith smith removed the Team:Infra Monitoring UI - DEPRECATED DEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_services label Jul 7, 2022
@botelastic botelastic bot added the needs-team Issues missing a team label label Jul 7, 2022
@nickofthyme nickofthyme added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc EUI Team:SharedUX Team label for AppEx-SharedUX (formerly Global Experience) labels Jul 8, 2022
@elasticmachine
Copy link
Contributor

Pinging @elastic/eui-design (EUI)

@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

@botelastic botelastic bot removed the needs-team Issues missing a team label label Jul 8, 2022
@pgayvallet
Copy link
Contributor

pgayvallet commented Sep 13, 2022

From the slack discussion opened by @1Copenut

To be maximally effective for screen readers and keyboard navigation, these items should be the first element to take focus,

Mostly FMI here, but IIRC from when I was doing a11y, the aria-live and/or role=alert elements don't have to be focusable, and therefor don't have to be at the top of the document? Or do they? Not asking about the skip link here, but about the EuiScreenReaderLive and what it should output.

these items should be the first element to take focus, and the first element users can tab to in the DOM source order.

Should not be an issue, we should be able to find a place in the chrome (the static part of the UI with the header and left nav) for these element to be first on focus.

The problem doing so is that they will be out of scope/range from individual application, meaning that is any context required for these components depends on the currently mounted app, Core will have to expose APIs for application to consume to populate this context.

a screen reader only DIV to announce route changes

This one could be tricky, depending on how you see it implemented. See, technically Kibana is not a regular React App. The root dom level, managed by Core (that we call chrome) has its own React mountpoint, and when an application is loaded (dashboard, ML...), they render their own application with their own React mount / react tree inside a container element. This means that Core isn't necessarily aware of intra app navigations (navigating from page A to page B in a given app) as there are 2 nested routers, which means having it automated at Core's level could be problematic.

Do know know exactly what the live element should output, and how you see the component hooked to history changes?

a skip link.

Where should the skip link direct to? If it's to an element that doesn't depend on the currently displayed Kibana application (e.g the element where Core does mount the current app), here

<AppWrapper chromeVisible$={chrome.getIsVisible$()}>
{/* Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header */}
<div id="app-fixed-viewport" />
{/* The actual plugin/app */}
{appComponent}
</AppWrapper>
)

This shouldn't be an issue. If the link's target should be dynamic depending on the currently mounted App, this could be more bothersome, given the currently mounted application would need to propagate the info about where the link should point to to Core for us to update the link depending on it.

@1Copenut
Copy link
Contributor Author

@pgayvallet Thank you for the clear, thorough responses. I'll restate questions and answer using the same format.

Adding @jennypavlova to this thread. She's started work on this item for the Infrastructure team, and brought the questions about source order placement. Her work is the first implementation of this new pattern. My hope is to not cause her too much rework, and to have a solution that works for all teams.

Question 1

Mostly FMI here, but IIRC from when I was doing a11y, the aria-live and/or role=alert elements don't have to be focusable, and therefor don't have to be at the top of the document? Or do they? Not asking about the skip link here, but about the EuiScreenReaderLive and what it should output.

I tried a few different solutions with and without setting focus on the EuiScreenReaderLive component, and ultimately settled on setting focus because it always moved me back to the top of the page, helped smooth out different screen reader + browser quirks about live regions, and made it possible to always press TAB once to reach the skip link, just like a browser handles full page refreshes for server-side pages that include skip links.

Question 2

Should not be an issue, we should be able to find a place in the chrome (the static part of the UI with the header and left nav) for these element to be first on focus.

The problem doing so is that they will be out of scope/range from individual application, meaning that is any context required for these components depends on the currently mounted app, Core will have to expose APIs for application to consume to populate this context.

So far I have been advising to use the <title> text string to populate the SR-only rendered div. This follows convention that screen readers announce the page title on full, server-rendered pages. Is there a way to expose the page title to consuming React apps with current infrastructure, or would that have to be new APIs exposed by the Core team? If it's new work, I will follow up with a meeting request to discuss options and/or alternatives.

Question 3

This one could be tricky, depending on how you see it implemented. See, technically Kibana is not a regular React App. The root dom level, managed by Core (that we call chrome) has its own React mountpoint, and when an application is loaded (dashboard, ML...), they render their own application with their own React mount / react tree inside a container element. This means that Core isn't necessarily aware of intra app navigations (navigating from page A to page B in a given app) as there are 2 nested routers, which means having it automated at Core's level could be problematic.

Do know know exactly what the live element should output, and how you see the component hooked to history changes?

The live element will output HTML like the following snippet. The rendered output can also be observed by inspecting source in the public EUI docs pages.

<!-- https://eui.elastic.co/#/utilities/accessibility#screen-reader-live-region -->

<div class="css-hus3oj-euiScreenReaderOnly" tabindex="-1">
  <div
    aria-atomic="true"
    aria-live="off"
    role="status"
  >
    Accessibility — Elastic UI Framework
  </div>
  <div
    aria-atomic="true"
    aria-hidden="true"
    aria-live="off"
    role="status"
  >
</div>

Question 4

Where should the skip link direct to? If it's to an element that doesn't depend on the currently displayed Kibana application (e.g the element where Core does mount the current app), here

If the div[app-fixed-viewport] is consistent and available on all Kibana apps, that could be a perfect target for the skip to content link to point. It would need a tabindex="-1" attribute and we'd need to test to make sure the focus halo didn't appear wrapping the entire screen, but otherwise that would be an excellent candidate!

@pgayvallet
Copy link
Contributor

Question 1

helped smooth out different screen reader + browser quirks about live regions

Thanks, make sense. I do remember that SR/browser behavior consistency was a nightmare at the time.

Question 2 + Question 3

So far I have been advising to use the <title> text string to populate the SR-only rendered div. [...] Is there a way to expose the page title to consuming React apps with current infrastructure, or would that have to be new APIs exposed by the Core team?

The title of the page can be set by applications using the chrome.docTitle.set core API. There isn't currently a way to retrieve this value, but we could easily add a core API for that, something like chrome.docTitle.get$ returning an observable emitting when the page title changes.

Note that if the SR-only div is supposed to just output the title of the page, then it can be implemented directly in our chrome and managed internally by Core without the need for other teams to do anything.

Question 4

If the div[app-fixed-viewport] is consistent and available on all Kibana apps, that could be a perfect target for the skip to content link to point

Yea, this div will always be present, and will always be the parent of any application currently displayed. If that works with you, then that means that this part too can be internally managed by Core without the need for other teams to do anything.

@jennypavlova
Copy link
Member

@1Copenut Thank you for adding me to the discussion.
I have one question regarding the document.title: I saw different approaches there - some titles have reverse order of the breadcrumbs and some are set custom (with different separators).

Which is the correct page title? Should it be for example:

  • Observability - Infrastructure - Settings ( the order of the breadcrumbs left to right) or
  • Settings - Infrastructure - Observability - Elastic (This structure is used on the APM page for example) or
  • a custom one: Metrics | Inventory - Kibana.

Which separator should we use - or a combination with | and -?
I think it's a good idea to have consistent titles in the apps so it will be easier for the user when navigating between pages.

Regarding the way the title is set:

The title of the page can be set by applications using the chrome.docTitle.set core API. There isn't currently a way to retrieve this value, but we could easily add a core API for that, something like chrome.docTitle.get$ returning an observable emitting when the page title changes.

Note that if the SR-only div is supposed to just output the title of the page, then it can be implemented directly in our chrome and managed internally by Core without the need for other teams to do anything.

@pgayvallet You mentioned that the title should be set with chrome.docTitle.set but what about the chrome.docTitle.change method or setting the document.title directly? I saw that both ways are used in many places to change the title.
Will thechange method work instead of set for this case if SR-only div is implemented directly in chrome?

@cee-chen
Copy link
Contributor

cee-chen commented Sep 14, 2022

Sorry to jump in here, I just have an opinion about:

Where should the skip link direct to?

If the div[app-fixed-viewport] is consistent and available on all Kibana apps, that could be a perfect target for the skip to content link to point.

Yea, this div will always be present, and will always be the parent of any application currently displayed.

My 2c: just because this div is wrapped around the parent app doesn't mean it should necessarily be used as the skip to content target. Almost all Kibana apps have side navs that would qualify as "non-main content" and usually should be skipped past. IMO, we should prefer the following order of operations:

  1. Allow consumers to set a custom target id element for the skip link to target, similar to destinationId in EuiSkipLink, in case apps have custom page structure/landmarks

  2. Look for a main tag, followed by a role="main" attribute, and target that if it exists (the new Eui/KibanaPageTemplate now always includes a main tag as a part of the template, so apps using KibanaPageTemplate will immediately benefit from this. Usages of the old template used role="main", I believe, so that should be a secondary target)

  3. Fall back to div[app-fixed-viewport] if none of the above exists

Hope that helps!

@1Copenut
Copy link
Contributor Author

Great points @constancecchen. I see this as an opportunity to capture your guidance in the EUI Getting Started > Accessibility section. I'll take that as an action item.

@pgayvallet
Copy link
Contributor

pgayvallet commented Sep 20, 2022

You mentioned that the title should be set with chrome.docTitle.set but what about the chrome.docTitle.change method

Sorry, I meant chrome.docTitle.change, not .set

or setting the document.title directly?

It's against our guidelines and shouldn't be done, specifically for scenarios like this where we need to have an unified way to track changes to the title. Changes performed by setting document.title = directly will not be known by Core and therefor Core would not be able to update the content of the SR component.

I saw that both ways are used in many places to change the title

Sigh... what can I say, using document.title = XXX shouldn't be done anywhere in our codebase 🤷. It would be blocker for the proposed technical solution, so we would need to open issues for teams to use the equivalent Core API instead.

@pgayvallet
Copy link
Contributor

My 2c: just because this div is wrapped around the parent app doesn't mean it should necessarily be used as the skip to content target. Almost all Kibana apps have side navs that would qualify as "non-main content" and usually should be skipped past.

Yea, that's what I thought too, which is why I asked the question. Would have been too easy otherwise 😅

we should prefer the following order of operations:

Allow consumers to set a custom target id element for the skip link to target,

It implies more structural Core changes. Let's say that as long as this ID is static per full application and therefore can be specified during the core.application.register call, it would be fine-ish. Any more complex API (e.g if the ID can change depending on the page the application is displaying) would be quite more complex and ideally avoided.

Al alternative to that would be to specify an arbitrary id value that would be the target of the skip link, and let teams set it up whenever they want. It's less elegant for sure, but would greatly simplify the actual implementation of 'where should the link point to'

  1. Look for a main tag, followed by a role="main" 3. Fall back to div[app-fixed-viewport] if none of the above exists

I may be wrong, but IIRC, an a11y skiplink must be a valid a tag pointing to an anchor, and cannot be executing logic to decide where it should direct to. Or did the specification evolve since due to SPAs?

@1Copenut
Copy link
Contributor Author

Excellent discussion, and I feel we're getting closer to a workable spec with each comment.

It implies more structural Core changes. Let's say that as long as this ID is static per full application and therefore can be specified during the core.application.register call, it would be fine-ish. Any more complex API (e.g if the ID can change depending on the page the application is displaying) would be quite more complex and ideally avoided.

Al alternative to that would be to specify an arbitrary id value that would be the target of the skip link, and let teams set it up whenever they want. It's less elegant for sure, but would greatly simplify the actual implementation of 'where should the link point to'

In the ideal scenario there would be one ID value that all teams point their skip link href to. We could then leave it to teams to define where in the source order that ID lives. If our ID value was "skip-target" then one team could place it on a first child DIV inside the main container, another team could drop further into the main content past a block of navigational links if that made for a better UX.

With that said, I think we'll be further ahead if we can use your first option, to have an ID that's static per full application, thus can be specified during the core.application.register call. I can appreciate this is more work. It also feels like an easier sell to teams because they have a bit more control to define what "their" ID value would be.

I may be wrong, but IIRC, an a11y skiplink must be a valid a tag pointing to an anchor, and cannot be executing logic to decide where it should direct to. Or did the specification evolve since due to SPAs?

You're right, it should be a valid link with a valid href attribute. Our EuiSkipLink component renders a proper a tag as you've specified. Consumers are required to pass a destinationId: string prop that renders the skip link's href attribute. There's an overrideLinkBehavior: boolean prop that can be set to True to correctly handle hashRouters.

Looking through WCAG SC 2.4.1 - Bypass Blocks again, I don't see anything our component is doing that would run counter to the guidance. SPAs definitely created some new use cases.

@pgayvallet
Copy link
Contributor

Consumers are required to pass a destinationId: string

Yea this would be fine. I was mostly asking because of the 3 step workflow described by @constancecchen in #135760 (comment) doesn't always target the element by an ID.

@cee-chen
Copy link
Contributor

cee-chen commented Sep 21, 2022

I may be wrong, but IIRC, an a11y skiplink must be a valid a tag pointing to an anchor, and cannot be executing logic to decide where it should direct to. Or did the specification evolve since due to SPAs?

There is not anything inherently more accessible about a skip link with a href than a button with a onClick. If the text of both clearly communicate what's going to happen (focus is going to move to some other content), a href is not superior in and of itself. VO/SRs will read out "link" instead of "button", but that's literally it. It's not like the SR will read out the ID that the href link is targeting, or even knows where it will go. The only reason why a a tag with an #id would be preferable is for people running no-JS blockers, and that's kind of a moot point for SPAs/Kibana. 😛

But don't just take my word for it, here's the official W3 spec that has no mention/requirement of a tags:

A mechanism is available to bypass blocks of content that are repeated on multiple Web pages.

And WebAIM has even more interesting context on the flexibility of the spec:

[...] This does not necessarily require that a skip link be present. Beginning the main content with an <h1> or using a <main> region would be a sufficient "mechanism".

EDIT: After speaking to @1Copenut more about this on Slack, I was incorrect in my assumption about the accessibility distinctions between a and button - some SRs will put a tags in a "Links menu" and buttons in a "Form controls" menu, so there is a UX/SR flow difference. My proposal to mitigate that is to leave the link as an a tag but with an empty href (if no ID is passed), and to intercept the onClick, preventDefault, and manually focus the fallback element.

@pgayvallet
Copy link
Contributor

Okay, thanks for the clarifications @constancecchen. This is good news, as it will allow us to be more flexible and allow potential 'fallback' on the target of the skipLink.

@1Copenut
Copy link
Contributor Author

The update to skip link behavior was merged into EUI 68.0.0. We should have the pieces we need in place after this version of EUI is merged into Kibana.

@vadimkibana
Copy link
Contributor

@1Copenut
Copy link
Contributor Author

Thank you @vadimkibana for putting these together. I'll add a few comments and a link to an app I built as a best case example.

@1Copenut
Copy link
Contributor Author

I've added #149847 for documentation on how to test with screen readers and assigned myself.

@vadimkibana
Copy link
Contributor

This should be closed now by #150461

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
EUI Meta Project:Accessibility Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:SharedUX Team label for AppEx-SharedUX (formerly Global Experience) WCAG A
Projects
None yet
Development

No branches or pull requests

10 participants