Skip to content

Commit

Permalink
Fix: CSS to animate popups, dialog for accessibility (fixes #600) (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverfoster authored Jan 14, 2025
1 parent a1b6c57 commit c82b122
Show file tree
Hide file tree
Showing 21 changed files with 365 additions and 173 deletions.
9 changes: 6 additions & 3 deletions js/a11y.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,16 @@ class A11y extends Backbone.Controller {
const config = this.config;
$element = $($element).first();

const isInDOM = Boolean($element.parents('body').length);
if (!isInDOM) return false;

const isOutsideOpenPopup = this.isPopupOpen && !this.popupStack?.lastItem[0]?.contains($element[0]);
if (isOutsideOpenPopup) return null;

const $branch = checkParents
? $element.add($element.parents())
: $element;

const isInDOM = Boolean($element.parents('body').length);
if (!isInDOM) return false;

const isNotVisible = $branch.toArray().some(item => {
const style = window.getComputedStyle(item);
// make sure item is not explicitly invisible
Expand Down
13 changes: 12 additions & 1 deletion js/a11y/popup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Adapt from 'core/js/adapt';
import logging from '../logging';

/**
* Tabindex and aria-hidden manager for popups.
Expand Down Expand Up @@ -94,6 +95,12 @@ export default class Popup extends Backbone.Controller {
}
this._floorStack.push($popupElement);
this._focusStack.push($(document.activeElement));
if ($popupElement.is('dialog')) {
$popupElement[0].addEventListener('cancel', event => event.preventDefault());
$popupElement[0].showModal();
return;
}
logging.deprecated('a11y/popup opened: Use native dialog tag for', $popupElement);
let $elements = $(config._options._tabbableElements).filter(config._options._tabbableElementsExcludes);
const $branch = $popupElement.add($popupElement.parents());
const $siblings = $branch.siblings().filter(config._options._tabbableElementsExcludes);
Expand Down Expand Up @@ -167,7 +174,11 @@ export default class Popup extends Backbone.Controller {
if (this._floorStack.length <= 1) {
return;
}
this._floorStack.pop();
const $popupElement = this._floorStack.pop();
if ($popupElement.is('dialog')) {
$popupElement[0].close();
return this._focusStack.pop();
}
$(config._options._tabbableElements).filter(config._options._tabbableElementsExcludes).each((index, item) => {
const $item = $(item);
let previousTabIndex = '';
Expand Down
1 change: 1 addition & 0 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'core/js/navigation';
import 'core/js/startController';
import 'core/js/DOMElementModifications';
import 'core/js/tooltips';
import 'core/js/shadow';
import 'plugins';

$('body').append(Handlebars.templates.loading());
Expand Down
1 change: 1 addition & 0 deletions js/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Drawer extends Backbone.Controller {

onAdaptStart() {
this._drawerView = new DrawerView({ collection: DrawerCollection });
this._drawerView.$el.insertAfter('#shadow');
}

onLanguageChanged() {
Expand Down
2 changes: 1 addition & 1 deletion js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ const helpers = {
*/
a11y_wrap_focus() {
const cfg = Adapt.config.get('_accessibility');
if (cfg._isPopupWrapFocusEnabled === false) return '';
if (cfg._options?._isPopupWrapFocusEnabled === false) return '';
return new Handlebars.SafeString('<a class="a11y-focusguard a11y-ignore a11y-ignore-focus" tabindex="0" role="presentation">&nbsp;</a>');
},

Expand Down
37 changes: 37 additions & 0 deletions js/shadow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Backbone from 'backbone';
import Adapt from 'core/js/adapt';
import ShadowView from './views/ShadowView';

class Shadow extends Backbone.Controller {

initialize() {
this.listenTo(Adapt, {
'adapt:start': this.onAdaptStart
});
}

onAdaptStart() {
this._shadowView = new ShadowView();
this._shadowView.$el.prependTo('body');
}

get isOpen() {
return this._shadowView?.isOpen ?? false;
}

async show() {
return this._shadowView?.showShadow();
}

async hide() {
return this._shadowView?.hideShadow();
}

remove() {
this._shadowView?.remove();
this._shadowView = null;
}

}

export default new Shadow();
8 changes: 8 additions & 0 deletions js/transitions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import logging from 'core/js/logging';

/**
* Wait for one requestAnimationFrame to allow classes to settle
* @returns {Promise}
*/
export async function transitionNextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}

/**
* Handler to await completion of active `CSSTransitions`.
* An optional `transition-property` to await can be specified, else all properties will be evaluated.
Expand Down
57 changes: 57 additions & 0 deletions js/views/ShadowView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Adapt from 'core/js/adapt';
import Backbone from 'backbone';
import { transitionNextFrame, transitionsEnded } from '../transitions';

class ShadowView extends Backbone.View {

className() {
return 'shadow js-shadow u-display-none';
}

attributes() {
return {
id: 'shadow'
};
}

initialize() {
this._isOpen = false;
this.disableAnimation = Adapt.config.get('_disableAnimation') ?? false;
this.$el.toggleClass('disable-animation', Boolean(this.disableAnimation));
this.render();
}

render() {
const template = Handlebars.templates.shadow;
this.$el.html(template({ _globals: Adapt.course.get('_globals') }));
return this;
}

get isOpen() {
return this._isOpen;
}

async showShadow() {
this._isOpen = true;
this.$el.addClass('anim-show-before');
await transitionNextFrame();
this.$el.removeClass('u-display-none');
await transitionNextFrame();
this.$el.addClass('anim-show-after');
await transitionsEnded(this.$el);
}

async hideShadow() {
this._isOpen = false;
this.$el.addClass('anim-hide-before');
await transitionNextFrame();
this.$el.addClass('anim-hide-after');
await transitionsEnded(this.$el);
this.$el.addClass('u-display-none');
await transitionNextFrame();
this.$el.removeClass('anim-open-before anim-open-after anim-hide-before anim-hide-after');
}

}

export default ShadowView;
Loading

0 comments on commit c82b122

Please sign in to comment.