Commit ff377ee4 authored by Curt Clemens's avatar Curt Clemens Committed by Chromium LUCI CQ

[Nearby] Fix Chromevox announcement order on high visibility page

The placement of the invisible IronA11yAnnouncer element was causing
trouble on the high visibility page with the apparent ordering of the
elements. Since it is a replacement for the page subtitle, it needed
to be located next to the subtitle in the html so that the node in the
accessibility tree gets placed correctly.

IronA11yAnnoucer was never designed to be used with modal dialogs.
Explicitly placing an IronA11yAnnouncer on the page was a hack to get
around the way modal dialogs are treated in the accessibility tree.
While this mostly worked, the iron-announce event continued to bubble
past the dialog and set the text on a second IronA11yAnnouncer element
on the underlying settings page causing some strange behavior after the
dialog is closed. Switching to a plain aria-live region fixes that.

Another issue with invisible stand-in elements was that the visual
highlight was not placed over the corresponding text when using
search + arrow keys. Using absolute positioning to place the
aria-live region on top of the subtitle it replaces achieves the
desired highlight: https://screenshot.googleplex.com/6dHPQMDYaMBYsQ3.png

When the page would initially load, Chromevox would read a few
elements that weren't on the currently active view of the
cr-view-manager. This turned out to be because elements with
aria-hidden="false" were overriding the "display: none" applied by the
view manager. Use "undefined" instead of "false" to fix.

Also correct an issue where the interval timer wasn't getting cleared
in the detached() event.

Fixed: 1165982
Change-Id: Ia31dd52629fc27db55906d88de2160d908e4fccb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2632085Reviewed-by: default avatarJames Vecore <vecore@google.com>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Commit-Queue: Curt Clemens <cclem@google.com>
Cr-Commit-Position: refs/heads/master@{#844953}
parent cb0b7772
...@@ -301,12 +301,15 @@ Polymer({ ...@@ -301,12 +301,15 @@ Polymer({
/** /**
* When the contact check boxes are visible, the contact name and description * When the contact check boxes are visible, the contact name and description
* can be aria-hidden since they are used as labels for the checkbox. * can be aria-hidden since they are used as labels for the checkbox.
* @return {string} Whether the contact name and description should be * @return {string|undefined} Whether the contact name and description should
* aria-hidden. Boolean converted to string, "true" or "false". * be aria-hidden. "true" or undefined.
* @private * @private
*/ */
getContactAriaHidden_() { getContactAriaHidden_() {
return this.showContactCheckBoxes_().toString(); if (this.showContactCheckBoxes_()) {
return 'true';
}
return undefined;
}, },
/** /**
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
margin-inline-start: var(--nearby-page-space-inline); margin-inline-start: var(--nearby-page-space-inline);
padding-block-end: 16px; padding-block-end: 16px;
padding-block-start: var(--nearby-page-space-block); padding-block-start: var(--nearby-page-space-block);
position: relative;
} }
#contentContainer { #contentContainer {
...@@ -45,6 +46,13 @@ ...@@ -45,6 +46,13 @@
margin: 0; margin: 0;
} }
#a11yAnnouncedPageSubTitle {
bottom: 16px;
clip: rect(0,0,0,0); /* Make invisible. Read on screen readers only. */
font-size: 14px;
position: absolute; /* Position directly over subtitle it replaces. */
}
#actions { #actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
...@@ -59,13 +67,16 @@ ...@@ -59,13 +67,16 @@
} }
</style> </style>
<div id="pageContainer" role="dialog" aria-modal="true" <div id="pageContainer" role="dialog" aria-modal="true"
aria-labelledby="[[getDialogAriaLabelledBy_(subTitleAriaHidden)]]"> aria-labelledby="[[getDialogAriaLabelledBy_(a11yAnnouncedSubTitle)]]">
<div id="header"> <div id="header">
<h1 id="pageTitle">[[title]]</h1> <h1 id="pageTitle">[[title]]</h1>
<h2 id="pageSubTitle" <h2 id="pageSubTitle"
aria-hidden$="[[getSubTitleAriaHidden_(subTitleAriaHidden)]]"> aria-hidden$="[[getSubTitleAriaHidden_(a11yAnnouncedSubTitle)]]">
[[subTitle]] [[subTitle]]
</h2> </h2>
<div id="a11yAnnouncedPageSubTitle" aria-live="polite">
[[a11yAnnouncedSubTitle]]
</div>
</div> </div>
<div id="contentContainer"> <div id="contentContainer">
......
...@@ -22,12 +22,14 @@ Polymer({ ...@@ -22,12 +22,14 @@ Polymer({
}, },
/** /**
* aria-hidden attribute for #subTitle div. * Alternate subtitle for screen readers. If not falsey, then the
* @type {boolean} * #pageSubTitle is aria-hidden and the #a11yAnnouncedPageSubTitle is
* rendered on screen readers instead. Changes to this value will result in
* aria-live announcements.
* @type {?string}
* */ * */
subTitleAriaHidden: { a11yAnnouncedSubTitle: {
type: Boolean, type: String,
value: false,
}, },
/** /**
...@@ -113,17 +115,21 @@ Polymer({ ...@@ -113,17 +115,21 @@ Polymer({
*/ */
getDialogAriaLabelledBy_() { getDialogAriaLabelledBy_() {
let labelIds = 'pageTitle'; let labelIds = 'pageTitle';
if (!this.subTitleAriaHidden) { if (!this.a11yAnnouncedSubTitle) {
labelIds += ' pageSubTitle'; labelIds += ' pageSubTitle';
} }
return labelIds; return labelIds;
}, },
/** /**
* @return {string} aria-hidden value for the #subTitle div * @return {string|undefined} aria-hidden value for the #subTitle div.
* 'true' or undefined.
* @private * @private
*/ */
getSubTitleAriaHidden_() { getSubTitleAriaHidden_() {
return this.subTitleAriaHidden.toString(); if (this.a11yAnnouncedSubTitle) {
return 'true';
}
return undefined;
}, },
}); });
...@@ -77,7 +77,8 @@ ...@@ -77,7 +77,8 @@
</style> </style>
<nearby-page-template title="$i18n{nearbyShareFeatureName}" <nearby-page-template title="$i18n{nearbyShareFeatureName}"
sub-title="[[getSubTitle_(deviceName, remainingTimeInSeconds_)]]" sub-title="[[getSubTitle_(deviceName, remainingTimeInSeconds_)]]"
sub-title-aria-hidden a11y-announced-sub-title="[[getA11yAnnouncedSubTitle_(deviceName,
remainingTimeInSeconds_)]]"
cancel-button-label="$i18n{cancel}" cancel-button-label="$i18n{cancel}"
close-only="[[getErrorTitle_(errorState_)]]"> close-only="[[getErrorTitle_(errorState_)]]">
<div id="content" slot="content"> <div id="content" slot="content">
...@@ -93,8 +94,6 @@ ...@@ -93,8 +94,6 @@
link-url="$i18n{nearbyShareLearnMoreLink}"> link-url="$i18n{nearbyShareLearnMoreLink}">
</settings-localized-link> </settings-localized-link>
</div> </div>
<!-- Announcer has to be inside modal dialog to work -->
<iron-a11y-announcer></iron-a11y-announcer>
</template> </template>
<template is="dom-if" if="[[getErrorTitle_(errorState_)]]"> <template is="dom-if" if="[[getErrorTitle_(errorState_)]]">
<iron-icon id="infoIcon" icon="nearby20:info" <iron-icon id="infoIcon" icon="nearby20:info"
......
...@@ -51,7 +51,6 @@ Polymer({ ...@@ -51,7 +51,6 @@ Polymer({
remainingTimeInSeconds_: { remainingTimeInSeconds_: {
type: Number, type: Number,
value: -1, value: -1,
observer: 'announceRemainingTime_',
}, },
/** @private {?nearbyShare.mojom.RegisterReceiveSurfaceResult} */ /** @private {?nearbyShare.mojom.RegisterReceiveSurfaceResult} */
...@@ -83,14 +82,11 @@ Polymer({ ...@@ -83,14 +82,11 @@ Polymer({
this.remainingTimeIntervalId_ = setInterval(() => { this.remainingTimeIntervalId_ = setInterval(() => {
this.calculateRemainingTime_(); this.calculateRemainingTime_();
}, 1000); }, 1000);
Polymer.IronA11yAnnouncer.requestAvailability();
this.announceRemainingTime_(this.remainingTimeInSeconds_);
}, },
/** @override */ /** @override */
detached() { detached() {
if (this.remainingTimeIntervalId_ === -1) { if (this.remainingTimeIntervalId_ !== -1) {
clearInterval(this.remainingTimeIntervalId_); clearInterval(this.remainingTimeIntervalId_);
this.remainingTimeIntervalId_ = -1; this.remainingTimeIntervalId_ = -1;
} }
...@@ -199,24 +195,26 @@ Polymer({ ...@@ -199,24 +195,26 @@ Polymer({
/** /**
* Announce the remaining time for screen readers. Only announce once per * Announce the remaining time for screen readers. Only announce once per
* minute to avoid overwhelming user. * minute to avoid overwhelming user. Though this gets called once every
* @param {number} remainingSeconds * second, the value returned only changes each minute.
* @return {string} The alternate page subtitle to be used as an aria-live
* announcement for screen readers.
* @private * @private
*/ */
announceRemainingTime_(remainingSeconds) { getA11yAnnouncedSubTitle_() {
// Skip announcement for 0 seconds left to avoid alerting on time out. // Skip announcement for 0 seconds left to avoid alerting on time out.
// There is a separate time out alert shown in the error section. // There is a separate time out alert shown in the error section.
if (remainingSeconds <= 0 || remainingSeconds % 60 !== 0) { if (this.remainingTimeInSeconds_ === 0) {
return; return '';
} }
const remainingMinutes = this.remainingTimeInSeconds_ > 0 ?
Math.ceil(this.remainingTimeInSeconds_ / 60) :
5;
const timeValue = this.i18n( const timeValue =
'nearbyShareHighVisibilitySubTitleMinutes', this.i18n('nearbyShareHighVisibilitySubTitleMinutes', remainingMinutes);
Math.ceil(this.remainingTimeInSeconds_ / 60));
const announcement = this.i18n( return this.i18n(
'nearbyShareHighVisibilitySubTitle', this.deviceName, timeValue); 'nearbyShareHighVisibilitySubTitle', this.deviceName, timeValue);
this.fire('iron-announce', {text: announcement});
}, },
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment