Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
588 changes: 445 additions & 143 deletions js/a11y.js

Large diffs are not rendered by default.

36 changes: 22 additions & 14 deletions js/a11y/ariaDisabled.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
/**
* @file Aria Disabled - Prevents interaction with aria-disabled elements
* @module core/js/a11y/ariaDisabled
* @description Intercepts keyboard and click events on elements marked with
* aria-disabled="true" to prevent their activation. Checks for aria-disabled
* on associated label 'for' attributes. Only responds to trusted user events.
*/

import Adapt from 'core/js/adapt';

/**
* Browser aria-disabled element interaction prevention
* @class
* @class AriaDisabled
* @classdesc Prevents activation of elements marked with aria-disabled="true".
* @extends Backbone.Controller
* @see https://github.com/adaptlearning/adapt_framework/issues/3097
* @see https://github.com/adaptlearning/adapt-contrib-core/issues/623
*/
export default class BrowserFocus extends Backbone.Controller {

export default class AriaDisabled extends Backbone.Controller {
initialize({ a11y }) {
this.a11y = a11y;
this._onKeyDown = this._onKeyDown.bind(this);
Expand All @@ -22,6 +32,14 @@ export default class BrowserFocus extends Backbone.Controller {
this.$body[0].addEventListener('click', this._onClick, true);
}

/**
* Checks if an element or its associated label is marked as aria-disabled.
* Searches up the DOM tree for aria-disabled="true" on the element itself
* or its parents.
* Checks if the element is an input with a label that has aria-disabled="true".
* @param {jQuery} $element - The jQuery-wrapped DOM element to check
* @returns {boolean} True if the element or its label is aria-disabled, false otherwise
*/
isAriaDisabled($element) {
// search element and parents for aria-disabled - see https://github.com/adaptlearning/adapt_framework/issues/3097
// search closest 'for' element for aria-disabled - see https://github.com/adaptlearning/adapt-contrib-core/issues/623
Expand All @@ -31,11 +49,6 @@ export default class BrowserFocus extends Backbone.Controller {
return isAriaDisabled;
}

/**
* Stop click handling on aria-disabled elements.
*
* @param {JQuery.Event} event
*/
_onClick(event) {
if (!event.isTrusted) return;
const $element = $(event.target);
Expand All @@ -44,11 +57,6 @@ export default class BrowserFocus extends Backbone.Controller {
event.stopImmediatePropagation();
}

/**
* Stop enter and space handling on aria-disabled elements.
*
* @param {JQuery.Event} event
*/
_onKeyDown(event) {
if (!event.isTrusted) return;
if (!['Enter', ' '].includes(event.key)) return;
Expand Down
23 changes: 21 additions & 2 deletions js/a11y/browserConfig.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
/**
* @file Browser Configuration - Accessibility browser feature detection
* @module core/js/a11y/browserConfig
* @description Handles browser-level accessibility configuration, particularly
* detecting and responding to user preferences like reduced motion settings.
* Applies appropriate CSS classes to the document based on browser capabilities
* and user preferences.
*
* **Responsibilities:**
* - Detects `prefers-reduced-motion` media query preference
* - Applies `is-prefers-reduced-motion` class to HTML element when enabled
* - Integrates with the A11y module configuration system
*
* @example
* import BrowserConfig from 'core/js/a11y/browserConfig';
* const browserConfig = new BrowserConfig({ a11y });
*/

import Adapt from '../adapt';

/**
* Browser configuration helper.
* @class
* @class BrowserConfig
* @classdesc Detects browser accessibility preferences and applies CSS class.
* @extends Backbone.Controller
*/
export default class BrowserConfig extends Backbone.Controller {

Expand Down
65 changes: 47 additions & 18 deletions js/a11y/browserFocus.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
/**
* @file Browser Focus - Accessibility focus handling modifications
* @module core/js/a11y/browserFocus
* @description Manages browser focus behavior for accessibility compliance.
* Handles focus movement when elements become disabled, hidden, or removed.
* Ensures screen readers properly track focus on click interactions.
*
* **Responsibilities:**
* - Moves focus forward when focused element becomes disabled/hidden/removed
* - Forces focus updates on click for screen reader compatibility
* - Manages `data-a11y-force-focus` attribute cleanup on blur
* - Adds click delay for screen reader focus processing when configured
*
* @example
* import BrowserFocus from 'core/js/a11y/browserFocus';
* const browserFocus = new BrowserFocus({ a11y });
*/

import Adapt from 'core/js/adapt';

/**
* Browser modifications to focus handling.
* @class
* @class BrowserFocus
* @classdesc Modifies browser focus behavior for accessibility and screen reader support.
* @extends Backbone.Controller
*/
export default class BrowserFocus extends Backbone.Controller {

Expand All @@ -16,6 +35,11 @@ export default class BrowserFocus extends Backbone.Controller {
});
}

/**
* Attaches blur and click event listeners to the document body.
* Uses event capturing for click to intercept before bubbling.
* @private
*/
_attachEventListeners() {
this.$body
.on('blur', '*', this._onBlur)
Expand All @@ -25,12 +49,12 @@ export default class BrowserFocus extends Backbone.Controller {
}

/**
* When any element in the document receives a blur event,
* check to see if it needs the `data-a11y-force-focus` attribute removing
* and check to see if it was blurred because a disabled attribute was added.
* If a disabled attribute was added, the focus will be moved forward.
*
* @param {JQuery.Event} event
* Handles blur events to manage focus transitions.
* Removes `data-a11y-force-focus` attribute when element loses focus,
* and moves focus to next readable element if the blurred element
* became disabled, hidden, or was removed from the DOM.
* @param {jQuery.Event} event - The blur event
* @private
*/
_onBlur(event) {
const config = this.a11y.config;
Expand All @@ -51,33 +75,38 @@ export default class BrowserFocus extends Backbone.Controller {
if (isNotBodyHTMLOrLostFocus) {
return;
}
// Check if element losing focus is losing focus
// Check if element is losing focus
// due to the addition of a disabled class, display none, visibility hidden,
// or because it has been removed from the dom
// or because it has been removed from the DOM
const isNotDisabledHiddenOrDetached = (!$element.is('[disabled]') && $element.css('display') !== 'none' && $element.css('visibility') !== 'hidden' && $element.parents('html').length);
if (isNotDisabledHiddenOrDetached) {
// the element is still available, refocus
// this can happen when jaws screen reader on role=group takes enter click
// when the focus was on the input element
// The element is still available, refocus
// This can happen when JAWS screen reader on `role="group"` takes enter click
// when the focus was on the input element
this._refocusCurrentActiveElement();
return;
}
// Move focus to next readable element
this.a11y.focusNext($element);
}

/**
* Refocuses the current active element without scrolling.
* Prevents JAWS screen reader from scrolling when focus is temporarily lost.
* @private
*/
_refocusCurrentActiveElement() {
const element = this.a11y.currentActiveElement;
if (!element) return;
// refocus on the existing active element to stop jaws from scrolling
this.a11y.focus(element, { preventScroll: true });
}

/**
* Force focus when clicked on a tabbable element,
* making sure `document.activeElement` is updated.
*
* @param {JQuery.Event} event
* Handles click events to force focus updates for screen readers.
* Ensures `document.activeElement` is updated when clicking tabbable elements.
* Delay click to allow screen readers to process focus changes.
* @param {MouseEvent} event - The click event (uses native event for isTrusted check)
* @private
*/
_onClick(event) {
if (!event.isTrusted) return;
Expand Down
43 changes: 39 additions & 4 deletions js/a11y/deprecated.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
/**
* @file Deprecated A11y API - Backward compatibility shims for legacy jQuery methods
* @module core/js/a11y/deprecated
* @description Provides backward compatibility by mapping deprecated jQuery accessibility
* methods to the new A11y module API. All deprecated methods log warnings directing
* developers to the modern replacements.
*
* **Method Categories:**
* - **Removed**: Methods that no longer serve a purpose (log removal notice, return stub)
* - **Deprecated**: Methods that map to new API equivalents (log warning, call new API)
*
* **jQuery Instance Methods ($.fn):**
* - `a11y_on` → `a11y.findTabbable()` + `a11y.toggleAccessible()`
* - `a11y_popup` → `a11y.popupOpened()`
* - `a11y_cntrl` → `a11y.toggleAccessible()` + `a11y.toggleEnabled()`
* - `a11y_cntrl_enabled` → `a11y.toggleAccessibleEnabled()`
* - `isReadable` → `a11y.isReadable()`
* - `focusNoScroll` → `a11y.focus()`
* - `focusNext` → `a11y.focusNext()` or `a11y.findFirstReadable()`
* - `focusOrNext` → `a11y.focusFirst()`
* - `a11y_focus` → `a11y.focusFirst()`
* - `scrollDisable` → `a11y.scrollDisable()`
* - `scrollEnable` → `a11y.scrollEnable()`
*
* **jQuery Static Methods ($):**
* - `a11y_on` → `a11y.toggleHidden()`
* - `a11y_popdown` → `a11y.popupClosed()`
* - `a11y_focus` → `a11y.focusFirst()`
* - `a11y_normalize` → `a11y.normalize()`
* - `a11y_remove_breaks` → `a11y.removeBreaks()`
*/

/**
* Registers deprecated jQuery methods that map to the new A11y API.
* Called during A11y module initialization to maintain backward compatibility.
* @param {Object} a11y - The A11y module instance for API delegation and logging
*/
export default function(a11y) {

/**
* The old API is rerouted to the new API with warnings.
*/

// Extend jQuery prototype with deprecated instance methods
Object.assign($.fn, {

isFixedPostion() {
Expand Down Expand Up @@ -124,6 +158,7 @@ export default function(a11y) {

});

// Extend jQuery static object with deprecated static methods
Object.assign($, {

a11y_alert() {
Expand Down
36 changes: 27 additions & 9 deletions js/a11y/focusOptions.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
/**
* @file Focus Options - Configuration object for A11y focus methods
* @module core/js/a11y/focusOptions
* @description Provides a standardized options container for focus-related methods
* in the A11y module.
* Encapsulates scroll prevention and deferred focus settings.
*
* @example
* import FocusOptions from 'core/js/a11y/focusOptions';
*
* const options = new FocusOptions({ preventScroll: true, defer: true });
* a11y.focus(element, options);
*/

/**
* @class FocusOptions
* @classdesc Configuration container for A11y focus method options.
*/
export default class FocusOptions {

/**
* Options parser for focus functions.
* @param {Object} options
* @param {boolean} [options.preventScroll=false] Stops the browser from scrolling to the focused point. https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
* @param {boolean} [options.defer=false] Add a defer to the focus call, allowing for user interface settling.
* Creates a FocusOptions instance with normalized default values.
* @param {Object} [options={}] - Focus configuration options
* @param {boolean} [options.preventScroll=false] - Prevents browser scrolling to focused element
* @param {boolean} [options.defer=false] - Defers focus call to allow UI settling
*/
constructor({
preventScroll = false,
defer = false
} = {}) {
/**
* Stops the browser from scrolling to the focused point.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
* @type {boolean}
*/
* Prevents browser scrolling to focused element.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus|MDN HTMLElement.focus()}
* @type {boolean}
*/
this.preventScroll = preventScroll;
/**
* Add a defer to the focus call, allowing for user interface settling.
* Defers the focus call, allowing UI to settle.
* @type {boolean}
*/
this.defer = defer;
Expand Down
36 changes: 29 additions & 7 deletions js/a11y/keyboardFocusOutline.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
/**
* @file Keyboard Focus Outline - Input method-aware focus outline management
* @module core/js/a11y/keyboardFocusOutline
* @description Controls focus outline visibility based on user input method.
* Hides focus outlines for mouse users while preserving them for keyboard navigation,
* improving visual aesthetics without sacrificing accessibility.
*
* **Behavior Modes:**
* - **Keyboard-only outlines**: Hidden by default, shown when navigation keys pressed
* - **Disabled outlines**: Focus outlines completely removed (not recommended)
* - **Default**: Focus outlines always visible
*
* **Trigger Keys:** Tab, Enter, Space, Arrow keys
*
* @example
* import KeyboardFocusOutline from 'core/js/a11y/keyboardFocusOutline';
* const focusOutline = new KeyboardFocusOutline({ a11y });
*/
import Adapt from 'core/js/adapt';

/**
* Manages whether or not the focus outline should be entirely removed
* or removed until a key is pressed on a tabbable element.
* @class
* @class KeyboardFocusOutline
* @classdesc Toggles focus outline visibility based on keyboard vs mouse input.
* @extends Backbone.Controller
*/
export default class KeyboardFocusOutline extends Backbone.Controller {

Expand Down Expand Up @@ -31,7 +49,9 @@ export default class KeyboardFocusOutline extends Backbone.Controller {
}

/**
* Add styling classes if required.
* Applies initial focus outline styling based on configuration.
* Adds `a11y-disable-focusoutline` class if outlines should be hidden.
* @private
*/
_start() {
const config = this.a11y.config;
Expand All @@ -46,9 +66,11 @@ export default class KeyboardFocusOutline extends Backbone.Controller {
}

/**
* Handle key down events for on a tabbable element.
*
* @param {JQuery.Event} event
* Handles keydown events to show focus outline on keyboard navigation.
* Removes `a11y-disable-focusoutline` class when a trigger key is pressed
* on a tabbable element that isn't in the ignore list.
* @param {KeyboardEvent} event - The keydown event
* @private
*/
_onKeyDown(event) {
const config = this.a11y.config;
Expand Down
Loading
Loading