[Accessibility] Route changes announcement (#150461)

Co-authored-by: Constance Chen <constance.chen@elastic.co>
Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>
This commit is contained in:
Rachel Shen 2023-02-24 09:46:58 -07:00 committed by GitHub
parent d973dad3a4
commit d2453beee0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 633 additions and 162 deletions

View file

@ -1,188 +1,208 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header renders 1`] = `
<header
class="hide-for-sharing headerGlobalNav"
data-test-subj="headerGlobalNav"
>
Array [
<div
class="header__bars"
id="globalHeaderBars"
class="emotion-euiScreenReaderOnly"
tabindex="-1"
>
<div
class="euiHeader euiHeader--dark euiHeader--fixed header__firstBar"
data-fixed-header="true"
aria-atomic="true"
aria-hidden="true"
aria-live="off"
role="status"
/>
<div
aria-atomic="true"
aria-live="off"
role="status"
>
test - Elastic
</div>
</div>,
<header
class="hide-for-sharing headerGlobalNav"
data-test-subj="headerGlobalNav"
>
<div
class="header__bars"
id="globalHeaderBars"
>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
class="euiHeader euiHeader--dark euiHeader--fixed header__firstBar"
data-fixed-header="true"
>
<div
class="euiHeaderSectionItem"
>
<a
aria-label="Elastic home"
class="euiHeaderLogo"
data-test-subj="logo"
href="/"
>
<span
class="chrHeaderLogo__cluster"
data-euiicon-type="logoElastic"
data-test-subj="globalLoadingIndicator-hidden"
>
Elastic Logo
</span>
<svg
aria-hidden="true"
aria-labelledby="elasticMark"
class="chrHeaderLogo__mark"
fill="none"
height="19"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title
id="elasticMark"
>
Elastic
</title>
<path
d="M9.74 16.882l.711-.076.046 1.455c-1.879.257-3.485.393-4.818.393-1.773 0-3.03-.515-3.773-1.545C1.164 16.08.8 14.473.8 12.306c0-4.333 1.727-6.5 5.167-6.5 1.666 0 2.909.47 3.727 1.394.818.924 1.227 2.394 1.227 4.379l-.106 1.409H2.664c0 1.364.242 2.379.742 3.03.5.652 1.349.985 2.576.985 1.242.03 2.485-.015 3.757-.121zm-.667-5.349c0-1.515-.243-2.59-.728-3.212-.484-.621-1.272-.94-2.363-.94s-1.924.334-2.47.986c-.545.651-.833 1.712-.848 3.166h6.409zM13.497 18.427V.7h1.848v17.727h-1.848zM27.027 9.806v6.076c0 .621 1.53.742 1.53.742l-.09 1.637c-1.303 0-2.38.106-3.03-.515a10.861 10.861 0 01-4.44.924c-1.136 0-2-.319-2.59-.97-.592-.636-.895-1.56-.895-2.773 0-1.197.303-2.09.91-2.651.605-.56 1.56-.925 2.863-1.046l3.879-.363v-1.06c0-.834-.182-1.44-.546-1.804-.363-.364-.863-.545-1.485-.545H18.27V5.82h4.742c1.394 0 2.41.318 3.046.97.651.636.97 1.651.97 3.015zm-7.606 5.03c0 1.516.621 2.273 1.879 2.273a9.89 9.89 0 003.303-.56l.56-.197v-4.076l-3.65.348c-.743.06-1.274.273-1.607.637-.333.363-.485.894-.485 1.575zM34.255 7.473c-1.788 0-2.697.62-2.697 1.879 0 .575.212.984.62 1.227.41.242 1.35.485 2.819.742 1.47.258 2.5.606 3.106 1.076.606.454.91 1.318.91 2.59 0 1.274-.41 2.198-1.228 2.789-.818.59-2 .894-3.576.894-1.015 0-4.424-.38-4.424-.38l.106-1.605c1.954.182 3.379.333 4.333.333.955 0 1.682-.151 2.182-.454.5-.303.758-.819.758-1.53 0-.713-.212-1.198-.637-1.455-.424-.258-1.363-.5-2.818-.728-1.455-.227-2.485-.56-3.09-1.015-.607-.439-.91-1.272-.91-2.47 0-1.196.424-2.09 1.273-2.666.848-.576 1.909-.864 3.166-.864 1 0 4.485.258 4.485.258v1.621c-1.833-.106-3.333-.242-4.379-.242zM47.952 7.685h-3.925v5.909c0 1.409.106 2.348.303 2.788.212.44.697.666 1.47.666l2.197-.151.121 1.53c-1.106.182-1.94.273-2.515.273-1.288 0-2.167-.318-2.667-.94-.5-.62-.742-1.818-.742-3.575v-6.5h-1.758V6.079h1.758V2.29h1.833v3.773h3.925v1.62zM50.527 3.276V1.139h1.849v2.152l-1.849-.015zm0 15.151V6.08h1.849v12.348h-1.849zM60.406 5.821c.546 0 1.47.106 2.773.303l.59.076-.075 1.5c-1.318-.152-2.288-.227-2.91-.227-1.393 0-2.348.333-2.848 1-.5.666-.757 1.909-.757 3.712 0 1.803.227 3.06.697 3.773.47.712 1.44 1.06 2.924 1.06l2.91-.227.075 1.53c-1.53.227-2.682.349-3.44.349-1.924 0-3.257-.5-3.984-1.485-.728-.985-1.106-2.652-1.106-5 0-2.349.393-4 1.181-4.94.803-.939 2.122-1.424 3.97-1.424z"
/>
</svg>
</a>
</div>
</div>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiHeaderSectionItem"
/>
</div>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiHeaderSectionItem"
/>
<div
class="euiHeaderSectionItem"
/>
<div
class="euiHeaderSectionItem"
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="helpMenuButton"
id="headerHelpMenu"
class="euiHeaderSectionItem"
>
<div
class="euiPopover__anchor css-16vtueo-render"
<a
aria-label="Elastic home"
class="euiHeaderLogo"
data-test-subj="logo"
href="/"
>
<button
aria-expanded="false"
aria-haspopup="true"
aria-label="Help menu"
class="euiButtonEmpty euiHeaderSectionItemButton css-wvaqcf-empty-text"
type="button"
<span
class="chrHeaderLogo__cluster"
data-euiicon-type="logoElastic"
data-test-subj="globalLoadingIndicator-hidden"
>
<span
class="euiButtonContent euiButtonEmpty__content"
Elastic Logo
</span>
<svg
aria-hidden="true"
aria-labelledby="elasticMark"
class="chrHeaderLogo__mark"
fill="none"
height="19"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title
id="elasticMark"
>
<span
class="euiButtonEmpty__text"
>
<span
class="euiHeaderSectionItemButton__content"
>
<span
data-euiicon-type="help"
/>
</span>
</span>
</span>
</button>
</div>
Elastic
</title>
<path
d="M9.74 16.882l.711-.076.046 1.455c-1.879.257-3.485.393-4.818.393-1.773 0-3.03-.515-3.773-1.545C1.164 16.08.8 14.473.8 12.306c0-4.333 1.727-6.5 5.167-6.5 1.666 0 2.909.47 3.727 1.394.818.924 1.227 2.394 1.227 4.379l-.106 1.409H2.664c0 1.364.242 2.379.742 3.03.5.652 1.349.985 2.576.985 1.242.03 2.485-.015 3.757-.121zm-.667-5.349c0-1.515-.243-2.59-.728-3.212-.484-.621-1.272-.94-2.363-.94s-1.924.334-2.47.986c-.545.651-.833 1.712-.848 3.166h6.409zM13.497 18.427V.7h1.848v17.727h-1.848zM27.027 9.806v6.076c0 .621 1.53.742 1.53.742l-.09 1.637c-1.303 0-2.38.106-3.03-.515a10.861 10.861 0 01-4.44.924c-1.136 0-2-.319-2.59-.97-.592-.636-.895-1.56-.895-2.773 0-1.197.303-2.09.91-2.651.605-.56 1.56-.925 2.863-1.046l3.879-.363v-1.06c0-.834-.182-1.44-.546-1.804-.363-.364-.863-.545-1.485-.545H18.27V5.82h4.742c1.394 0 2.41.318 3.046.97.651.636.97 1.651.97 3.015zm-7.606 5.03c0 1.516.621 2.273 1.879 2.273a9.89 9.89 0 003.303-.56l.56-.197v-4.076l-3.65.348c-.743.06-1.274.273-1.607.637-.333.363-.485.894-.485 1.575zM34.255 7.473c-1.788 0-2.697.62-2.697 1.879 0 .575.212.984.62 1.227.41.242 1.35.485 2.819.742 1.47.258 2.5.606 3.106 1.076.606.454.91 1.318.91 2.59 0 1.274-.41 2.198-1.228 2.789-.818.59-2 .894-3.576.894-1.015 0-4.424-.38-4.424-.38l.106-1.605c1.954.182 3.379.333 4.333.333.955 0 1.682-.151 2.182-.454.5-.303.758-.819.758-1.53 0-.713-.212-1.198-.637-1.455-.424-.258-1.363-.5-2.818-.728-1.455-.227-2.485-.56-3.09-1.015-.607-.439-.91-1.272-.91-2.47 0-1.196.424-2.09 1.273-2.666.848-.576 1.909-.864 3.166-.864 1 0 4.485.258 4.485.258v1.621c-1.833-.106-3.333-.242-4.379-.242zM47.952 7.685h-3.925v5.909c0 1.409.106 2.348.303 2.788.212.44.697.666 1.47.666l2.197-.151.121 1.53c-1.106.182-1.94.273-2.515.273-1.288 0-2.167-.318-2.667-.94-.5-.62-.742-1.818-.742-3.575v-6.5h-1.758V6.079h1.758V2.29h1.833v3.773h3.925v1.62zM50.527 3.276V1.139h1.849v2.152l-1.849-.015zm0 15.151V6.08h1.849v12.348h-1.849zM60.406 5.821c.546 0 1.47.106 2.773.303l.59.076-.075 1.5c-1.318-.152-2.288-.227-2.91-.227-1.393 0-2.348.333-2.848 1-.5.666-.757 1.909-.757 3.712 0 1.803.227 3.06.697 3.773.47.712 1.44 1.06 2.924 1.06l2.91-.227.075 1.53c-1.53.227-2.682.349-3.44.349-1.924 0-3.257-.5-3.984-1.485-.728-.985-1.106-2.652-1.106-5 0-2.349.393-4 1.181-4.94.803-.939 2.122-1.424 3.97-1.424z"
/>
</svg>
</a>
</div>
</div>
<div
class="euiHeaderSectionItem"
/>
</div>
</div>
<div
class="euiHeader euiHeader--default euiHeader--fixed header__secondBar"
data-fixed-header="true"
>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiHeaderSectionItem euiHeaderSectionItem--borderRight header__toggleNavButtonSection"
>
<button
aria-controls="generated-id"
aria-expanded="false"
aria-label="Toggle primary navigation"
aria-pressed="false"
class="euiButtonEmpty euiHeaderSectionItemButton css-wvaqcf-empty-text"
data-test-subj="toggleNavButton"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
<span
class="euiHeaderSectionItemButton__content"
>
<span
data-euiicon-type="menu"
/>
</span>
</span>
</span>
</button>
</div>
</div>
<nav
aria-label="Breadcrumbs"
class="euiBreadcrumbs euiHeaderBreadcrumbs emotion-euiHeaderBreadcrumbs"
data-test-subj="breadcrumbs"
>
<ol
class="euiBreadcrumbs__list emotion-euiBreadcrumbs__list-isTruncated"
>
<li
class="euiBreadcrumb emotion-euiBreadcrumb-application-isTruncated"
data-test-subj="euiBreadcrumb"
>
<span
aria-current="page"
class="euiBreadcrumb__content emotion-euiBreadcrumb__content-application-isTruncatedLast-onlyChild-euiTextColor-default"
data-test-subj="breadcrumb first last"
title="test"
>
test
</span>
</li>
</ol>
</nav>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--right"
>
<div
class="euiHeaderSectionItem"
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
data-test-subj="headerAppActionMenu"
class="euiHeaderSectionItem"
/>
</div>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiHeaderSectionItem"
/>
<div
class="euiHeaderSectionItem"
/>
<div
class="euiHeaderSectionItem"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="helpMenuButton"
id="headerHelpMenu"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-expanded="false"
aria-haspopup="true"
aria-label="Help menu"
class="euiButtonEmpty euiHeaderSectionItemButton css-wvaqcf-empty-text"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
<span
class="euiHeaderSectionItemButton__content"
>
<span
data-euiicon-type="help"
/>
</span>
</span>
</span>
</button>
</div>
</div>
</div>
<div
class="euiHeaderSectionItem"
/>
</div>
</div>
<div
class="euiHeader euiHeader--default euiHeader--fixed header__secondBar"
data-fixed-header="true"
>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--left"
>
<div
class="euiHeaderSectionItem euiHeaderSectionItem--borderRight header__toggleNavButtonSection"
>
<button
aria-controls="generated-id"
aria-expanded="false"
aria-label="Toggle primary navigation"
aria-pressed="false"
class="euiButtonEmpty euiHeaderSectionItemButton css-wvaqcf-empty-text"
data-test-subj="toggleNavButton"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
<span
class="euiHeaderSectionItemButton__content"
>
<span
data-euiicon-type="menu"
/>
</span>
</span>
</span>
</button>
</div>
</div>
<nav
aria-label="Breadcrumbs"
class="euiBreadcrumbs euiHeaderBreadcrumbs emotion-euiHeaderBreadcrumbs"
data-test-subj="breadcrumbs"
>
<ol
class="euiBreadcrumbs__list emotion-euiBreadcrumbs__list-isTruncated"
>
<li
class="euiBreadcrumb emotion-euiBreadcrumb-application-isTruncated"
data-test-subj="euiBreadcrumb"
>
<span
aria-current="page"
class="euiBreadcrumb__content emotion-euiBreadcrumb__content-application-isTruncatedLast-onlyChild-euiTextColor-default"
data-test-subj="breadcrumb first last"
title="test"
>
test
</span>
</li>
</ol>
</nav>
<div
class="euiHeaderSection euiHeaderSection--dontGrow euiHeaderSection--right"
>
<div
class="euiHeaderSectionItem"
>
<div
data-test-subj="headerAppActionMenu"
/>
</div>
</div>
</div>
</div>
</div>
</header>
</header>,
]
`;

View file

@ -0,0 +1,317 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScreenReaderRouteAnnouncements renders 1`] = `
<ScreenReaderRouteAnnouncements
appId$={
BehaviorSubject {
"_value": "test",
"closed": false,
"currentObservers": null,
"hasError": false,
"isStopped": false,
"observers": Array [
SafeSubscriber {
"_finalizers": Array [
Subscription {
"_finalizers": null,
"_parentage": [Circular],
"closed": false,
"initialTeardown": [Function],
},
],
"_parentage": null,
"closed": false,
"destination": ConsumerObserver {
"partialObserver": Object {
"complete": undefined,
"error": undefined,
"next": [Function],
},
},
"initialTeardown": undefined,
"isStopped": false,
},
],
"thrownError": null,
}
}
breadcrumbs$={
BehaviorSubject {
"_value": Array [
Object {
"text": "Visualize",
},
],
"closed": false,
"currentObservers": null,
"hasError": false,
"isStopped": false,
"observers": Array [
SafeSubscriber {
"_finalizers": Array [
Subscription {
"_finalizers": null,
"_parentage": [Circular],
"closed": false,
"initialTeardown": [Function],
},
],
"_parentage": null,
"closed": false,
"destination": ConsumerObserver {
"partialObserver": Object {
"complete": undefined,
"error": undefined,
"next": [Function],
},
},
"initialTeardown": undefined,
"isStopped": false,
},
],
"thrownError": null,
}
}
customBranding$={
BehaviorSubject {
"_value": Object {},
"closed": false,
"currentObservers": null,
"hasError": false,
"isStopped": false,
"observers": Array [
SafeSubscriber {
"_finalizers": Array [
Subscription {
"_finalizers": null,
"_parentage": [Circular],
"closed": false,
"initialTeardown": [Function],
},
],
"_parentage": null,
"closed": false,
"destination": ConsumerObserver {
"partialObserver": Object {
"complete": undefined,
"error": undefined,
"next": [Function],
},
},
"initialTeardown": undefined,
"isStopped": false,
},
],
"thrownError": null,
}
}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
>
<EuiScreenReaderLive
focusRegionOnTextChange={true}
>
<EuiScreenReaderOnly>
<div
css="unknown styles"
key="null"
tabIndex={-1}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"hus3oj-euiScreenReaderOnly": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-emotion="css"
data-s=""
>
.emotion-euiScreenReaderOnly{position:absolute;inset-block-start:auto;inset-inline-start:-10000px;inline-size:1px;block-size:1px;clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);overflow:hidden;margin:-1px;}
</style>
<style
data-styled="active"
data-styled-version="5.1.0"
/>
</head>,
"ctr": 1,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.emotion-euiScreenReaderOnly{position:absolute;inset-block-start:auto;inset-inline-start:-10000px;inline-size:1px;block-size:1px;clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);overflow:hidden;margin:-1px;}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "hus3oj-euiScreenReaderOnly",
"next": undefined,
"styles": ";
// Take the element out of the layout
position: absolute;
// Keep it vertically inline
inset-block-start: auto;
// Chrome requires a left value, and Selenium (used by Kibana's FTR) requires an off-screen position for its .getVisibleText() to not register SR-only text
inset-inline-start: -10000px;
// The element must have a size (for some screen readers)
inline-size: 1px;
block-size: 1px;
// But reduce the visible size to nothing
clip: rect(0 0 0 0);
clip-path: inset(50%);
// And ensure no overflows occur
overflow: hidden;
// Chrome requires the negative margin to not cause overflows of parent containers
margin: -1px;
;label:euiScreenReaderOnly;;;;",
"toString": [Function],
}
}
/>
<div
className="emotion-euiScreenReaderOnly"
tabIndex={-1}
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="off"
role="status"
/>
<div
aria-atomic="true"
aria-live="off"
role="status"
>
Visualize - Elastic
</div>
</div>
</div>
</EuiScreenReaderOnly>
</EuiScreenReaderLive>
</ScreenReaderRouteAnnouncements>
`;

View file

@ -46,6 +46,7 @@ import { HeaderActionMenu } from './header_action_menu';
import { HeaderExtension } from './header_extension';
import { HeaderTopBanner } from './header_top_banner';
import { HeaderMenuButton } from './header_menu_button';
import { ScreenReaderRouteAnnouncements } from './screen_reader_a11y';
export interface HeaderProps {
kibanaVersion: string;
@ -108,6 +109,12 @@ export function Header({
return (
<>
<ScreenReaderRouteAnnouncements
breadcrumbs$={observables.breadcrumbs$}
customBranding$={customBranding$}
appId$={application.currentAppId$}
/>
<HeaderTopBanner headerBanner$={observables.headerBanner$} />
<header className={className} data-test-subj="headerGlobalNav">
<div id="globalHeaderBars" className="header__bars">

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { ScreenReaderRouteAnnouncements } from './screen_reader_a11y';
import { mount } from 'enzyme';
describe('ScreenReaderRouteAnnouncements', () => {
it('renders', () => {
const component = mountWithIntl(
<ScreenReaderRouteAnnouncements
appId$={new BehaviorSubject('test')}
customBranding$={new BehaviorSubject({})}
breadcrumbs$={new BehaviorSubject([{ text: 'Visualize' }])}
/>
);
expect(component).toMatchSnapshot();
});
it('does not set the focusOnRegionOnTextChange for canvas or discover', () => {
const noFocusComponentCanvas = mount(
<ScreenReaderRouteAnnouncements
appId$={new BehaviorSubject('canvas')}
customBranding$={new BehaviorSubject({})}
breadcrumbs$={new BehaviorSubject([])}
/>
);
const noFocusComponentDiscover = mount(
<ScreenReaderRouteAnnouncements
appId$={new BehaviorSubject('discover')}
customBranding$={new BehaviorSubject({})}
breadcrumbs$={new BehaviorSubject([])}
/>
);
expect(
noFocusComponentCanvas
.debug()
.includes('<EuiScreenReaderLive focusRegionOnTextChange={false}>')
).toBeTruthy();
expect(
noFocusComponentDiscover
.debug()
.includes('<EuiScreenReaderLive focusRegionOnTextChange={false}>')
).toBeTruthy();
});
it('sets the focusOnRegionOnTextChange to true for other apps', () => {
const noFocusComponent = mount(
<ScreenReaderRouteAnnouncements
appId$={new BehaviorSubject('visualize')}
customBranding$={new BehaviorSubject({})}
breadcrumbs$={new BehaviorSubject([])}
/>
);
expect(
noFocusComponent.debug().includes('<EuiScreenReaderLive focusRegionOnTextChange={true}>')
).toBeTruthy();
});
});

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, useState, useEffect } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiScreenReaderLive } from '@elastic/eui';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { HeaderProps } from './header';
const DEFAULT_BRAND = 'Elastic'; // This may need to be DRYed out with https://github.com/elastic/kibana/blob/main/packages/core/rendering/core-rendering-server-internal/src/views/template.tsx#L34
const SEPARATOR = ' - ';
export const ScreenReaderRouteAnnouncements: FC<{
breadcrumbs$: HeaderProps['breadcrumbs$'];
customBranding$: HeaderProps['customBranding$'];
appId$: InternalApplicationStart['currentAppId$'];
}> = ({ breadcrumbs$, customBranding$, appId$ }) => {
const [routeTitle, setRouteTitle] = useState('');
const branding = useObservable(customBranding$)?.pageTitle || DEFAULT_BRAND;
const breadcrumbs = useObservable(breadcrumbs$, []);
useEffect(() => {
if (breadcrumbs.length) {
const breadcrumbText: string[] = [];
// Reverse the breadcrumb title order and ensure we only pick up valid strings
[...breadcrumbs].reverse().forEach((breadcrumb) => {
if (typeof breadcrumb.text === 'string') breadcrumbText.push(breadcrumb.text);
});
breadcrumbText.push(branding);
setRouteTitle(breadcrumbText.join(SEPARATOR));
} else {
// Don't announce anything during loading states
setRouteTitle('');
}
}, [breadcrumbs, branding]);
// 1. Canvas dynamically updates breadcrumbs *and* page title/history on every name onChange,
// which leads to focus fighting if this is enabled
// 2. Discover has custom h1 focus behavior on route change, which should probably
// be removed in favor of this for a more consistent SR experience
const appId = useObservable(appId$);
const disableFocusForApps = ['canvas', 'discover'];
const focusRegionOnTextChange = !disableFocusForApps.includes(appId || '');
return (
<EuiScreenReaderLive focusRegionOnTextChange={focusRegionOnTextChange}>
{routeTitle}
</EuiScreenReaderLive>
);
};