Skip to content
This repository has been archived by the owner on Jan 4, 2023. It is now read-only.

New A11Y metrics #216

Merged
merged 7 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions custom_metrics/a11y.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,184 @@ return JSON.stringify({
screen_reader_classes: captureAndLogError(() => {
return document.querySelectorAll('.sr-only, .visually-hidden').length > 0;
}),
form_control_a11y_tree: captureAndLogError(() => {
function doesMatchAny(string, regexps) {
let found = false;
for (const regexp of regexps) {
if (regexp.test(string)) {
return true;
}
}

return false;
}

const attributes_to_track = [
/^aria-.+$/,
/^type$/,
/^id$/,
/^name$/,
/^placeholder$/,
/^accept$/,
/^autocomplete$/,
/^autofocus$/,
/^capture$/,
/^max$/,
/^maxlength$/,
/^min$/,
/^minlength$/,
/^required$/,
/^readonly$/,
/^pattern$/,
/^multiple$/,
/^step$/,
];
function addControlToStats(node, accumulator) {
const control_stats = {
type: node.node_info.nodeType.toLowerCase(),
attributes: {},
properties: {},
accessible_name: node.name.value || '',
accessible_name_sources: [],
role: node.role.value || '',
};

// Store all attribute information
for (let [key, value] of Object.entries(node.node_info.attributes || {})) {
key = key.toLowerCase();
if (!doesMatchAny(key, attributes_to_track)) {
continue;
}

control_stats.attributes[key] = value;
}

for (let property of node.properties) {
control_stats.properties[property.name] = property.value.value;
}

for (let source of node.name.sources || []) {
// Only include sources that contributed a value
if (!source.value || !source.value.value) {
continue;
}

// Only keep the relevant properties
const cleaned_source = {
type: source.type,
value: source.value.value,
};
if (source.attribute) {
cleaned_source.attribute = source.attribute;
}

control_stats.accessible_name_sources.push(cleaned_source);
}

accumulator.push(control_stats);
}

const allowed_control_types = [
'input',
'select',
'textarea',
'button',
];

const stats_of_controls = [];
for (const node of $WPT_ACCESSIBILITY_TREE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest declaring a const at the top of the file that aliases $WPT_ACCESSIBILITY_TREE, so that there's only one version of it that needs to be expanded in case anyone wants to add a new custom metric that depends on it.

Copy link
Contributor Author

@foxdavidj foxdavidj Jun 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change made in latest commit. This what you were envisioning?

const node_type = (node.node_info?.nodeType || '').toLowerCase();
if (!allowed_control_types.includes(node_type)) {
continue;
}

addControlToStats(node, stats_of_controls);
}

return stats_of_controls;
}),
// If the radio or checkbox elements are inside a fieldset or legend
fieldset_radio_checkbox: captureAndLogError(() => {
let total_radio_in_fieldset = 0;
let total_checkbox_in_fieldset = 0;
const fieldset_stats = [];

const fieldset_elements = document.querySelectorAll('fieldset');
for (let fieldset of fieldset_elements) {
let has_legend = !!fieldset.querySelector('legend');
const total_radio = fieldset.querySelectorAll('input[type="radio"]').length
const total_checkbox = fieldset.querySelectorAll('input[type="checkbox"]').length

total_radio_in_fieldset += total_radio;
total_checkbox_in_fieldset += total_checkbox;

fieldset_stats.push({
has_legend,
total_radio,
total_checkbox,
});
}

return {
total_radio: document.querySelectorAll('input[type="radio"]').length,
total_checkbox: document.querySelectorAll('input[type="checkbox"]').length,
total_radio_in_fieldset,
total_checkbox_in_fieldset,

fieldsets: fieldset_stats,
}
}),
required_form_controls: captureAndLogError(() => {
function getVisibleLabel(element) {
// Explicit label
const id = (element.getAttribute('id') || '').trim();
if (id.length > 0) {
const element = document.querySelector(`label[for="${id}"]`);
if (element) {
return element.textContent.trim();
}
}

// Implicit label
if (element.parentElement && element.parentElement.tagName === 'LABEL') {
return element.parentElement.textContent.trim();
}

return null;
}

function hasRequiredAsterisk(element) {
const label = getVisibleLabel(element);
if (!label) {
return false;
}

if (label.substr(0, 1) === '*' || label.substr(-1, 1) === '*') {
return true;
}

return false;
}

const controls = document.querySelectorAll('input, select, textarea');
const required_stats = [];
for (const control of controls) {
const has_visible_required_asterisk = hasRequiredAsterisk(control);
const has_required = control.hasAttribute('required');
const has_aria_required = control.hasAttribute('aria-required');

// Only include stats for controls that are required in some fashion
if (!has_required && !has_aria_required && !has_visible_required_asterisk) {
continue;
}

required_stats.push({
has_visible_required_asterisk,
has_required,
has_aria_required,
});
}

return required_stats;
}),
});
46 changes: 44 additions & 2 deletions custom_metrics/almanac.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,21 +587,63 @@ return JSON.stringify({
};
const parsed_videos = parseNodes(videos, filter_options);

// Count the number of video elements that have a track element
let total_videos_with_track_element = 0;
for (let video of videos) {
if (video.querySelector('track')) {
total_videos_with_track_element++;
}
}

const parsed_tracks = parseNodes(tracks, {max_prop_length: 255});
parsed_videos.total_with_track = total_videos_with_track_element;
parsed_videos.tracks = parsed_tracks;
return parsed_videos;
})(),

'audios': (() => {
const audios = document.querySelectorAll('audio');
const tracks = document.querySelectorAll('audio track');

const filter_options = {
include_only_prop_list: [
/^autoplay$/,
/^controls$/,
/^loop$/,
/^muted$/,
/^poster$/,
/^preload$/,
/^aria-.+$/,
],
// Protect us from weird values
max_prop_length: 255,
};
const parsed_audios = parseNodes(audios, filter_options);

// Count the number of audio elements that have a track element
let total_audios_with_track_element = 0;
for (let audio of audios) {
if (audio.querySelector('track')) {
total_audios_with_track_element++;
}
}

const parsed_tracks = parseNodes(tracks, {max_prop_length: 255});
parsed_audios.total_with_track = total_audios_with_track_element;
parsed_audios.tracks = parsed_tracks;
return parsed_audios;
})(),

'iframes': (() => {
const iframes = document.querySelectorAll("iframe");
const iframes_using_loading = [
...document.querySelectorAll("iframe[loading]"),
];

/** @type {ParseNodeOptions} */
return {
iframes: parseNodes(iframes),

loading_values: iframes_using_loading.map((iframe) => {
const value = iframe.getAttribute("loading") || "";
return value.toLocaleLowerCase().replace(/\s+/gm, " ").trim();
Expand Down
23 changes: 13 additions & 10 deletions custom_metrics/metric-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ Example response:

Stats of `<video>` and `<track>` elements.

`total_with_track` contains the total number of `<video>` elements had at least one `<track>` element

Example response:

```json
Expand All @@ -432,6 +434,7 @@ Example response:
"poster": 1,
"src": 1
},
"total_with_track": 1,
"tracks": {
"total": 0,
"nodes": [],
Expand Down Expand Up @@ -712,13 +715,13 @@ A JSON array of `<img>` elements on the page.
Sample response:

```
{
"url": "https://placekitten.com/401/401",
"width": 401,
"height": 401,
"naturalWidth": 401,
"naturalHeight": 401,
"loading": "lazy",
"inViewport": true
}
```
{
"url": "https://placekitten.com/401/401",
"width": 401,
"height": 401,
"naturalWidth": 401,
"naturalHeight": 401,
"loading": "lazy",
"inViewport": true
}
```