Skip to content

Commit

Permalink
[Settings] Improve accessibility for add languages dialog
Browse files Browse the repository at this point in the history
Previously in crrev.com/c/4602528, we improved the performance settings
add exception dialog accessibility by introducing a wrapper component
around cr-checkbox. This CL extends that solution to the add languages
dialog so that it can also have the index and count of languages read
out by screenreaders. A side effect of this is that the visuals when
focusing a list item will be different - the entire row is highlighted
instead of just the checkbox element.

Before: https://screenshot.googleplex.com/98DrDqz4sfrPCd9.png
After: https://screenshot.googleplex.com/7xtDeM3e2qKissz.png

Bug: 1451532
Change-Id: I004ea4c76efa14eb0248c35162a324ef574b2d0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4928230
Reviewed-by: John Lee <johntlee@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Commit-Queue: Charles Meng <charlesmeng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1210310}
  • Loading branch information
Charles Meng authored and Chromium LUCI CQ committed Oct 16, 2023
1 parent ff24961 commit ab7f980
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 157 deletions.
2 changes: 1 addition & 1 deletion chrome/browser/resources/settings/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ build_webui("build") {
"clear_browsing_data_dialog/clear_browsing_data_dialog.ts",
"clear_browsing_data_dialog/history_deletion_dialog.ts",
"clear_browsing_data_dialog/passwords_deletion_dialog.ts",
"controls/settings_checkbox_list_entry.ts",
"controls/settings_checkbox.ts",
"downloads_page/downloads_page.ts",
"on_startup_page/on_startup_page.ts",
Expand All @@ -103,7 +104,6 @@ build_webui("build") {
"performance_page/speed_page.ts",
"performance_page/tab_discard_exception_add_dialog.ts",
"performance_page/tab_discard_exception_add_input.ts",
"performance_page/tab_discard_exception_current_sites_entry.ts",
"performance_page/tab_discard_exception_current_sites_list.ts",
"performance_page/tab_discard_exception_edit_dialog.ts",
"performance_page/tab_discard_exception_edit_input.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
<style include="settings-shared">
/* add padding so that checkbox ripple is not cut off */
.ripple-padding {
padding-inline-end: 20px;
padding-inline-start: 20px;
padding-inline-end: 20px;
}

cr-checkbox::part(label-container) {
min-width: 0;
}

.label-slot {
align-items: center;
display: flex;
}

.checkbox-label {
margin-inline-start: 10px;
}
</style>
<cr-checkbox id="checkbox" class="list-item no-outline ripple-padding"
tab-index="-1" checked="{{checked}}">
<div class="label-slot">
<site-favicon url="[[item]]"></site-favicon>
<div class="checkbox-label text-elide">[[item]]</div>
</div>
tab-index="-1" checked="{{checked}}" part="checkbox">
<slot></slot>
</cr-checkbox>
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
* @fileoverview 'settings-checkbox-list-entry' is a wrapper for cr-checkbox so
* that it can have the correct accessibility behavior while inside an
* iron-list. Because cr-checkbox passes its focus to its inner checkbox
* element, screen readers are unable to infer a parent-child relationship
* between the list element and the focused checkbox. As a result using the
* roles listbox/option and annotating with aria-setsize/aria-posinset will not
* work properly.
*
* To fix this 'settings-checkbox-list-entry' hijacks focus and prevents it from
* going into the inner element, so that screenreaders will properly read
* "(x of y)". This however changes the visuals so that when the element is
* focused the entire row is highlighted instead of just the checkbox.
*
* Example usage:
* <iron-list role="listbox" items="[[items]]">
* <template>
* <settings-checkbox-list-entry role="option"
* checked="[[isSelected_(item)]]"
* tabindex="[[tabIndex]]"
* aria-posinset$="[[addOneTo_(index)]]"
* aria-setsize$="[[items.length]]"
* on-change="toggleSelection_">
* [[item]]
* </settings-checkbox-list-entry>
* </template>
* </iron-list>
*/
import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import '../settings_shared.css.js';
import '../site_favicon.js';

import {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './settings_checkbox_list_entry.html.js';

export interface SettingsCheckboxListEntryElement {
$: {
checkbox: CrCheckboxElement,
};
}

export class SettingsCheckboxListEntryElement extends PolymerElement {
static get is() {
return 'settings-checkbox-list-entry';
}

static get template() {
return getTemplate();
}

static get properties() {
return {
// Used to set the status of the checkbox when the entry is created,
// as well as when the list item for the entry changes.
checked: {
type: Boolean,
value: false,
observer: 'onCheckedChanged_',
},

// Reflects to the tabindex attribute. When it is negative (non-focusable)
// aria-hidden will be set to "true", so that it will be ignored by screen
// readers. This is needed because iron-list recycles its entries, so when
// focusing an entry, the screen reader can be confused by other entries'
// aria-posinset and aria-setsize attributes if they aren't aria-hidden.
tabindex: {
type: Number,
value: 0,
observer: 'onTabIndexChanged_',
reflectToAttribute: true,
},
};
}

checked: boolean;
private posinset: number;
private setsize: number;
private tabindex: number;

override ready() {
super.ready();
this.addEventListener('click', this.onClick_);
this.addEventListener('keydown', this.onKeyDown_);
this.addEventListener('keyup', this.onKeyUp_);
}

private onClick_() {
this.$.checkbox.click();
}

// Handle key presses in the same way as cr-checkbox, because it no longer
// receives focus.
private onKeyDown_(e: KeyboardEvent) {
if (e.key !== ' ' && e.key !== 'Enter') {
return;
}

e.preventDefault();
e.stopPropagation();
if (e.repeat) {
return;
}

if (e.key === 'Enter') {
this.$.checkbox.click();
}
}

private onKeyUp_(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
}

if (e.key === ' ') {
this.$.checkbox.click();
}
}

private onCheckedChanged_() {
this.setAttribute('aria-checked', String(this.$.checkbox.checked));
}

private onTabIndexChanged_() {
this.setAttribute('aria-hidden', this.tabindex >= 0 ? 'false' : 'true');
}
}

declare global {
interface HTMLElementTagNameMap {
'settings-checkbox-list-entry': SettingsCheckboxListEntryElement;
}
}

customElements.define(
SettingsCheckboxListEntryElement.is, SettingsCheckboxListEntryElement);
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,18 @@
flex-direction: column;
height: 350px;
overflow: auto;
padding-inline-start: 0;
padding-inline-end: 0;
}

settings-checkbox-list-entry::part(checkbox) {
padding-inline-start: 40px;
}

#dialog-title > span,
iron-list {
flex: 1;
}

.ripple-padding {
/* Create a little extra space for checkbox ink ripple to flow into. */
padding-inline-start: 20px;
}

cr-checkbox::part(label-container) {
white-space: nowrap;
}
</style>
<cr-dialog id="dialog" close-text="$i18n{close}">
<div id="dialog-title" slot="title">
Expand All @@ -36,17 +32,19 @@
</cr-search-field>
</div>
<div id="dialog-body" slot="body" scrollable>
<iron-list class="ripple-padding" scroll-target="dialog-body"
role="grid" items="[[getLanguages_(filterValue_)]]"
aria-rowcount$="[[getLanguagesCount_(filterValue_)]]">
<iron-list scroll-target="dialog-body" role="listbox"
items="[[getLanguages_(filterValue_)]]">
<template>
<cr-checkbox class="list-item no-outline" role="row"
checked="[[willAdd_(item.code)]]" tab-index="[[tabIndex]]"
aria-rowindex$="[[getAriaRowindex_(index)]]"
<settings-checkbox-list-entry role="option"
checked="[[willAdd_(item.code)]]" tabindex="[[tabIndex]]"
aria-posinset$="[[getAriaPosinset_(index)]]"
aria-setsize$="[[getLanguagesCount_(filterValue_)]]"
aria-label="[[i18n('addLanguageAriaLabel', item.displayName)]]"
on-change="onLanguageCheckboxChange_">
[[getDisplayText_(item)]]
</cr-checkbox>
<div class="text-elide">
[[getDisplayText_(item)]]
</div>
</settings-checkbox-list-entry>
</template>
</iron-list>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@
* languages.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_search_field/cr_search_field.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import '../controls/settings_checkbox_list_entry.js';
import '../settings_shared.css.js';

import {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {CrScrollableMixin} from 'chrome://resources/cr_elements/cr_scrollable_mixin.js';
import {CrSearchFieldElement} from 'chrome://resources/cr_elements/cr_search_field/cr_search_field.js';
import {FindShortcutMixin} from 'chrome://resources/cr_elements/find_shortcut_mixin.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {SettingsCheckboxListEntryElement} from '../controls/settings_checkbox_list_entry.js';

import {getTemplate} from './add_languages_dialog.html.js';
import {LanguageHelper} from './languages_types.js';

Expand All @@ -33,6 +34,7 @@ export interface SettingsAddLanguagesDialogElement {
}

interface Repeaterevent extends Event {
target: SettingsCheckboxListEntryElement;
model: {
item: chrome.languageSettingsPrivate.Language,
};
Expand Down Expand Up @@ -131,8 +133,8 @@ export class SettingsAddLanguagesDialogElement extends
return this.getLanguages_().length;
}

/** @return A 1-based index for aria-rowindex. */
private getAriaRowindex_(index: number): number {
/** @return A 1-based index for aria-posinset. */
private getAriaPosinset_(index: number): number {
return index + 1;
}

Expand All @@ -156,7 +158,7 @@ export class SettingsAddLanguagesDialogElement extends
// iron-list re-uses a previous checkbox), and the checkbox can only be
// changed after that by user action.
const language = e.model.item;
if ((e.target as CrCheckboxElement).checked) {
if (e.target.checked) {
this.languagesToAdd_.add(language.code);
} else {
this.languagesToAdd_.delete(language.code);
Expand Down

This file was deleted.

0 comments on commit ab7f980

Please sign in to comment.