Skip to content

Commit

Permalink
Merge branch 'develop' into jmr-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeremy-Rivera authored Nov 26, 2024
2 parents 5cc523b + 8060ada commit 9b677be
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 47 deletions.
6 changes: 2 additions & 4 deletions lib/checks/keyboard/focusable-no-name-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { isFocusable } from '../../commons/dom';
import { isInTabOrder } from '../../commons/dom';
import { accessibleTextVirtual } from '../../commons/text';

function focusableNoNameEvaluate(node, options, virtualNode) {
const tabIndex = virtualNode.attr('tabindex');
const inFocusOrder = isFocusable(virtualNode) && tabIndex > -1;
if (!inFocusOrder) {
if (!isInTabOrder(virtualNode)) {
return false;
}

Expand Down
5 changes: 3 additions & 2 deletions lib/checks/keyboard/no-focusable-content-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import isFocusable from '../../commons/dom/is-focusable';
import { getRoleType } from '../../commons/aria';
import { parseTabindex } from '../../core/utils';

export default function noFocusableContentEvaluate(node, options, virtualNode) {
if (!virtualNode.children) {
Expand Down Expand Up @@ -51,6 +52,6 @@ function getFocusableDescendants(vNode) {
}

function usesUnreliableHidingStrategy(vNode) {
const tabIndex = parseInt(vNode.attr('tabindex'), 10);
return !isNaN(tabIndex) && tabIndex < 0;
const tabIndex = parseTabindex(vNode.attr('tabindex'));
return tabIndex !== null && tabIndex < 0;
}
6 changes: 4 additions & 2 deletions lib/checks/keyboard/tabindex-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { parseTabindex } from '../../core/utils';

function tabindexEvaluate(node, options, virtualNode) {
const tabIndex = parseInt(virtualNode.attr('tabindex'), 10);
const tabIndex = parseTabindex(virtualNode.attr('tabindex'));

// an invalid tabindex will either return 0 or -1 (based on the element) so
// will never be above 0
// @see https://www.w3.org/TR/html51/editing.html#the-tabindex-attribute
return isNaN(tabIndex) ? true : tabIndex <= 0;
return tabIndex === null || tabIndex <= 0;
}

export default tabindexEvaluate;
7 changes: 3 additions & 4 deletions lib/commons/dom/get-tabbable-elements.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { querySelectorAll } from '../../core/utils';
import { parseTabindex } from '../../core/utils';

/**
* Get all elements (including given node) that are part of the tab order
Expand All @@ -13,11 +14,9 @@ function getTabbableElements(virtualNode) {

const tabbableElements = nodeAndDescendents.filter(vNode => {
const isFocusable = vNode.isFocusable;
let tabIndex = vNode.actualNode.getAttribute('tabindex');
tabIndex =
tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null;
const tabIndex = parseTabindex(vNode.actualNode.getAttribute('tabindex'));

return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable;
return tabIndex !== null ? isFocusable && tabIndex >= 0 : isFocusable;
});

return tabbableElements;
Expand Down
3 changes: 2 additions & 1 deletion lib/commons/dom/inserted-into-focus-order.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import isFocusable from './is-focusable';
import isNativelyFocusable from './is-natively-focusable';
import { parseTabindex } from '../../core/utils';

/**
* Determines if an element is in the focus order, but would not be if its
Expand All @@ -12,7 +13,7 @@ import isNativelyFocusable from './is-natively-focusable';
* if its tabindex were removed. Else, false.
*/
function insertedIntoFocusOrder(el) {
const tabIndex = parseInt(el.getAttribute('tabindex'), 10);
const tabIndex = parseTabindex(el.getAttribute('tabindex'));

// an element that has an invalid tabindex will return 0 or -1 based on
// if it is natively focusable or not, which will always be false for this
Expand Down
9 changes: 3 additions & 6 deletions lib/commons/dom/is-focusable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import focusDisabled from './focus-disabled';
import isNativelyFocusable from './is-natively-focusable';
import { nodeLookup } from '../../core/utils';
import { parseTabindex } from '../../core/utils';

/**
* Determines if an element is keyboard or programmatically focusable.
Expand All @@ -23,10 +24,6 @@ export default function isFocusable(el) {
return true;
}
// check if the tabindex is specified and a parseable number
const tabindex = vNode.attr('tabindex');
if (tabindex && !isNaN(parseInt(tabindex, 10))) {
return true;
}

return false;
const tabindex = parseTabindex(vNode.attr('tabindex'));
return tabindex !== null;
}
3 changes: 2 additions & 1 deletion lib/commons/dom/is-in-tab-order.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nodeLookup } from '../../core/utils';
import isFocusable from './is-focusable';
import { parseTabindex } from '../../core/utils';

/**
* Determines if an element is focusable and able to be tabbed to.
Expand All @@ -16,7 +17,7 @@ export default function isInTabOrder(el) {
return false;
}

const tabindex = parseInt(vNode.attr('tabindex', 10));
const tabindex = parseTabindex(vNode.attr('tabindex'));
if (tabindex <= -1) {
return false; // Elements with tabindex=-1 are never in the tab order
}
Expand Down
10 changes: 4 additions & 6 deletions lib/core/base/context/create-frame-context.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { parseTabindex } from '../../utils';

export function createFrameContext(frame, { focusable, page }) {
return {
node: frame,
Expand All @@ -11,12 +13,8 @@ export function createFrameContext(frame, { focusable, page }) {
}

function frameFocusable(frame) {
const tabIndex = frame.getAttribute('tabindex');
if (!tabIndex) {
return true;
}
const int = parseInt(tabIndex, 10);
return isNaN(int) || int >= 0;
const tabIndex = parseTabindex(frame.getAttribute('tabindex'));
return tabIndex === null || tabIndex >= 0;
}

function getBoundingSize(domNode) {
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export { default as objectHasOwn } from './object-has-own';
export { default as parseCrossOriginStylesheet } from './parse-crossorigin-stylesheet';
export { default as parseSameOriginStylesheet } from './parse-sameorigin-stylesheet';
export { default as parseStylesheet } from './parse-stylesheet';
export { default as parseTabindex } from './parse-tabindex';
export { default as performanceTimer } from './performance-timer';
export { pollyfillElementsFromPoint } from './pollyfill-elements-from-point';
export { default as preloadCssom } from './preload-cssom';
Expand Down
22 changes: 22 additions & 0 deletions lib/core/utils/parse-tabindex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Parses a tabindex value to return an integer if valid, or null if invalid.
* @method parseTabindex
* @memberof axe.utils
* @param {string|null} str
* @return {number|null}
*/
function parseTabindex(value) {
if (typeof value !== 'string') {
return null;
}

// spec: https://html.spec.whatwg.org/#rules-for-parsing-integers
const match = value.trim().match(/^([-+]?\d+)/);
if (match) {
return Number(match[1]);
}

return null;
}

export default parseTabindex;
7 changes: 4 additions & 3 deletions lib/rules/autocomplete-matches.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sanitize } from '../commons/text';
import standards from '../standards';
import { isVisibleToScreenReaders, isVisibleOnScreen } from '../commons/dom';
import { parseTabindex } from '../core/utils';

function autocompleteMatches(node, virtualNode) {
const autocomplete = virtualNode.attr('autocomplete');
Expand Down Expand Up @@ -34,8 +35,8 @@ function autocompleteMatches(node, virtualNode) {
// The element has `tabindex="-1"` and has a [[semantic role]] that is
// not a [widget](https://www.w3.org/TR/wai-aria-1.1/#widget_roles)
const role = virtualNode.attr('role');
const tabIndex = virtualNode.attr('tabindex');
if (tabIndex === '-1' && role) {
const tabIndex = parseTabindex(virtualNode.attr('tabindex'));
if (tabIndex < 0 && role) {
const roleDef = standards.ariaRoles[role];
if (roleDef === undefined || roleDef.type !== 'widget') {
return false;
Expand All @@ -44,7 +45,7 @@ function autocompleteMatches(node, virtualNode) {

// The element is **not** visible on the page or exposed to assistive technologies
if (
tabIndex === '-1' &&
tabIndex < 0 &&
virtualNode.actualNode &&
!isVisibleOnScreen(virtualNode) &&
!isVisibleToScreenReaders(virtualNode)
Expand Down
6 changes: 4 additions & 2 deletions lib/rules/no-negative-tabindex-matches.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { parseTabindex } from '../core/utils';

function noNegativeTabindexMatches(node, virtualNode) {
const tabindex = parseInt(virtualNode.attr('tabindex'), 10);
return isNaN(tabindex) || tabindex >= 0;
const tabindex = parseTabindex(virtualNode.attr('tabindex'));
return tabindex === null || tabindex >= 0;
}

export default noNegativeTabindexMatches;
32 changes: 16 additions & 16 deletions locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"help": "Erforderliche ARIA-Attribute müssen bereitgestellt werden."
},
"aria-required-children": {
"description": "Stellt sicher, dass Elemente mit einer ARIA-Rolle, welche bestimmte untergeordnete Rollen voraussetzten auch diese enthalten.",
"description": "Stellt sicher, dass Elemente mit einer ARIA-Rolle, welche bestimmte untergeordnete Rollen voraussetzten diese auch enthalten.",
"help": "Bestimmte ARIA-Rollen müssen spezifische, untergeordnete Kind-Rollen enthalten."
},
"aria-required-parent": {
Expand Down Expand Up @@ -342,8 +342,8 @@
"help": "Die Seite muss eine Überschrift der ersten Ebene enthalten."
},
"presentation-role-conflict": {
"description": "Markiert Elemente welche eine Rolle besitzen, die none oder presentation ist und die eine Rollenauflösung benötigen.",
"help": "Elemente mit Rolle none oder presentation dürfen mit anderen Rollen nicht in Konflikt stehen."
"description": "Elemente mit role=\"none\" oder role=\"presentation\" sollten kein globales ARIA-Attribute besitzen oder fokussierbar sein, damit sie von Screenreadern ignoriert werden.",
"help": "Elemente mit \"role=none\" oder \"role=presentation\" sollen von Screenreadern ignoriert werden."
},
"region": {
"description": "Stellt sicher, dass jeglicher Inhalt in einer landmark region enthalten ist.",
Expand Down Expand Up @@ -465,7 +465,7 @@
"hidden": "aria-errormessage Wert `${data.values}` kann nicht auf ein verstecktes Element verweisen."
},
"incomplete": {
"singular": "Stellt sicher, dass aria-errormessage Wert `${data.values}` zu einem existierendem Element verweist.",
"singular": "Stellt sicher, dass aria-errormessage Wert `${data.values}` auf ein existierendes Element verweist.",
"plural": "Stellt sicher, dass aria-errormessage Werte `${data.values}` zu existierenden Elementen verweisen.",
"idrefs": "Es konnte nicht festgestellt werden, ob das Element aria-errormessage auf der Seite existiert: ${data.values}"
}
Expand Down Expand Up @@ -529,7 +529,7 @@
},
"aria-unsupported-attr": {
"pass": "ARIA Attribut wird unterstützt",
"fail": "ARIA Attribut ist nicht wirklich in Screenreadern und anderen assistiven Technologien unterstützt: ${data.values}"
"fail": "ARIA Attribut ist nicht allgemein in Screenreadern und anderen assistiven Technologien unterstützt: ${data.values}"
},
"aria-valid-attr-value": {
"pass": "ARIA Attributwerte sind gültig.",
Expand Down Expand Up @@ -602,7 +602,7 @@
},
"unsupportedrole": {
"pass": "ARIA Rolle wird unterstützt.",
"fail": "Folgende Rollen werden nicht wirklich in Screenreadern und assistiven Technologien unterstützt: ${data.values}"
"fail": "Folgende Rollen werden nicht allgemein in Screenreadern und assistiven Technologien unterstützt: ${data.values}"
},
"valid-scrollable-semantics": {
"pass": "Das Element hat eine gültige Semantik für ein Element in der Fokusreihenfolge.",
Expand Down Expand Up @@ -634,7 +634,7 @@
"color-contrast": {
"pass": {
"default": "Das Element hat einen ausreichenden Kontrast von ${data.contrastRatio}.",
"hidden": "Das Element ist verstec"
"hidden": "Das Element ist versteckt."
},
"fail": {
"default": "Das Element hat einen unzureichenden Kontrast von ${data.contrastRatio} (Vordergrundfarbe: ${data.fgColor}, Hintergrundfarbe: ${data.bgColor}, Schriftgröße: ${data.fontSize}, Schriftstärke: ${data.fontWeight}). Erwartetes Kontrastverhältnis von ${data.expectedContrastRatio}",
Expand Down Expand Up @@ -709,7 +709,7 @@
},
"focusable-modal-open": {
"pass": "Keine fokussierbaren Elemente während ein modaler Dialog offen ist.",
"incomplete": "Üerprüfe ob Elemente fokussierbar während des derzeitigen Status sind."
"incomplete": "Überprüfe ob Elemente während des derzeitigen Status fokussierbar sind."
},
"focusable-no-name": {
"pass": "Das Element befindet sich nicht in der Tabreihenfolge und enthält keinen zugänglichen Text.",
Expand Down Expand Up @@ -795,15 +795,15 @@
},
"multiple-label": {
"pass": "Das <form>-Element besitzt keine multiplen <label>-Elemente.",
"incomplete": "Unterstützung in assistiven Technologien von Elementen mit mehreren Labeln ist nicht wirklich gegeben. Es sollte sichergestellt werden, dass alle relevanten Informationen im ersten Label enthalten sind."
"incomplete": "Elemente mit mehreren Labeln werden in assistiven Technologien nicht allgemein unterstützt. Es sollte sichergestellt werden, dass alle relevanten Informationen im ersten Label enthalten sind."
},
"title-only": {
"pass": "Das <form>-Element ist nicht nur lediglich durch ein title-Attribut beschriftet.",
"fail": "Das <form>-Element ist lediglich durch ein title-Attribut beschriftet."
},
"landmark-is-unique": {
"pass": "Landmarks sollten eine einzigartige Rolle oder Rollen/Label/Titel (zugänglicher Name / accessible name) Kombination besitzen.",
"fail": "Landmark muss ein einzigartiges aria-label, aria-labelledby oder Titel besitzen um es von anderen zu unterscheiden."
"pass": "Landmarks besitzen eine einzigartige Rolle oder Rollen/Label/Titel (zugänglicher Name / accessible name) Kombination.",
"fail": "Landmark muss ein einzigartiges aria-label, aria-labelledby oder einen Titel besitzen, um es von anderen zu unterscheiden."
},
"has-lang": {
"pass": "Das <html>-Element besitzt ein lang-Attribut.",
Expand Down Expand Up @@ -981,7 +981,7 @@
"incomplete": "Ob das Element über Kindelemente bzw. textuelle Inhalte verfügt, kann nicht ermittelt werden."
},
"doc-has-title": {
"pass": "Test",
"pass": "Das Dokument besitzt ein nichtleeres <title>-Element.",
"fail": "Das Dokument besitzt kein <title>-Element oder das <title>-Element ist leer."
},
"exists": {
Expand Down Expand Up @@ -1096,9 +1096,9 @@
"fail": "Nicht alle (nichtleeren) Datenzellen haben eine Tabellenkopfzelle."
},
"td-headers-attr": {
"pass": "Das headers-Attribut wird ausschließlich dafür verwendet, um auf andere Zellen in der Tabelle zu verweisen.",
"pass": "Das headers-Attribut wird ausschließlich dafür verwendet, um auf andere Kopfzellen in der Tabelle zu verweisen.",
"incomplete": "Das headers-Attribut ist leer.",
"fail": "Das headers-Attribut wird nicht ausschließlich dafür verwendet, um auf andere Zellen in der Tabelle zu verweisen."
"fail": "Das headers-Attribut wird nicht ausschließlich dafür verwendet, um auf andere Kopfzellen in der Tabelle zu verweisen."
},
"th-has-data-cells": {
"pass": "Alle Tabellenkopfzellen beziehen sich auf Datenzellen.",
Expand All @@ -1108,7 +1108,7 @@
"hidden-content": {
"pass": "Jeglicher Inhalt der Seite wurde analysiert.",
"fail": "Beim Analysieren der Inhalte auf dieser Seite sind Probleme aufgetreten.",
"incomplete": "Auf der Seite befinden sich versteckte Inhalte, die nicht analysiert werden konnten. Um den Inhalt analysieren zu können, müssen Sie die Anzeige auslösen."
"incomplete": "Auf der Seite befinden sich versteckte Inhalte, die nicht analysiert werden konnten. Um den Inhalt analysieren zu können, müssen Sie die Anzeige dieser Inhalte auslösen."
}
},
"failureSummaries": {
Expand All @@ -1119,5 +1119,5 @@
"failureMessage": "Korrigiere alle der folgenden Punkte:{{~it:value}}\n {{=value.split('\\n').join('\\n ')}}{{~}}"
}
},
"incompleteFallbackMessage": ""
"incompleteFallbackMessage": "axe konnte den Grund nicht ermitteln. Versuchen Sie es mit dem Element Inspector."
}
39 changes: 39 additions & 0 deletions test/core/utils/parse-tabindex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
describe('axe.utils.parseTabindex', function () {
'use strict';

it('should return 0 for "0"', function () {
assert.strictEqual(axe.utils.parseTabindex('0'), 0);
});

it('should return 1 for "+1"', function () {
assert.strictEqual(axe.utils.parseTabindex('+1'), 1);
});

it('should return -1 for "-1"', function () {
assert.strictEqual(axe.utils.parseTabindex('-1'), -1);
});

it('should return null for null', function () {
assert.strictEqual(axe.utils.parseTabindex(null), null);
});

it('should return null for an empty string', function () {
assert.strictEqual(axe.utils.parseTabindex(''), null);
});

it('should return null for a whitespace string', function () {
assert.strictEqual(axe.utils.parseTabindex(' '), null);
});

it('should return null for non-numeric strings', function () {
assert.strictEqual(axe.utils.parseTabindex('abc'), null);
});

it('should return the first valid digit(s) for decimal numbers', function () {
assert.strictEqual(axe.utils.parseTabindex('2.5'), 2);
});

it('should return 123 for "123abc"', function () {
assert.strictEqual(axe.utils.parseTabindex('123abc'), 123);
});
});

0 comments on commit 9b677be

Please sign in to comment.