Skip to content

Commit

Permalink
Fix: CSS to animate popups, dialog for accessibility (fixes #600)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverfoster committed Oct 29, 2024
1 parent fda4c9e commit a65700b
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 163 deletions.
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
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
55 changes: 55 additions & 0 deletions js/views/ShadowView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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.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-open-before');
await transitionNextFrame();
this.$el.removeClass('u-display-none');
await transitionNextFrame();
this.$el.addClass('anim-open-after');
await transitionsEnded(this.$el);
}

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

}

export default ShadowView;
135 changes: 53 additions & 82 deletions js/views/drawerView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import Adapt from 'core/js/adapt';
import shadow from '../shadow';
import a11y from 'core/js/a11y';
import DrawerItemView from 'core/js/views/drawerItemView';
import Backbone from 'backbone';
import {
transitionNextFrame,
transitionsEnded
} from '../transitions';

class DrawerView extends Backbone.View {

tagName() {
return 'dialog';
}

className() {
return [
'drawer',
Expand All @@ -14,7 +23,6 @@ class DrawerView extends Backbone.View {

attributes() {
return {
role: 'dialog',
'aria-modal': 'true',
'aria-labelledby': 'drawer-heading',
'aria-hidden': 'true',
Expand All @@ -32,6 +40,7 @@ class DrawerView extends Backbone.View {
initialize() {
this._isVisible = false;
this.disableAnimation = Adapt.config.has('_disableAnimation') ? Adapt.config.get('_disableAnimation') : false;
this.$el.toggleClass('disable-animation', Boolean(this.disableAnimation));
this._globalDrawerPosition = Adapt.config.get('_drawer')?._position ?? 'auto';
this.drawerDuration = Adapt.config.get('_drawer')?._duration ?? 400;
this.setupEventListeners();
Expand All @@ -41,6 +50,7 @@ class DrawerView extends Backbone.View {
setupEventListeners() {
this.onKeyUp = this.onKeyUp.bind(this);
$(window).on('keyup', this.onKeyUp);
this.el.addEventListener('click', this.onShadowClicked.bind(this), { capture: true });
}

onKeyUp(event) {
Expand All @@ -49,18 +59,24 @@ class DrawerView extends Backbone.View {
this.hideDrawer();
}

onShadowClicked(event) {
const dialog = this.el;
const rect = dialog.getBoundingClientRect();
const isInDialog = (rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
if (isInDialog) return;
event.preventDefault();
this.hideDrawer();
}

render() {
const template = Handlebars.templates.drawer;
$(this.el).html(template({ _globals: Adapt.course.get('_globals') })).prependTo('body');
const shadowTemplate = Handlebars.templates.shadow;
$(shadowTemplate()).prependTo('body');
this.$el.html(template({ _globals: Adapt.course.get('_globals') }));
_.defer(this.postRender.bind(this));
return this;
}

postRender() {
this.$('a, button, input, select, textarea').attr('tabindex', -1);

this.checkIfDrawerIsAvailable();
}

Expand All @@ -74,7 +90,6 @@ class DrawerView extends Backbone.View {
.removeClass(`is-position-${this.drawerPosition}`)
.addClass(`is-position-${position}`);
this.drawerPosition = position;
this.drawerAnimationDir = (position === 'auto') ? (isRTL ? 'left' : 'right') : position;
}

openCustomView(view, hasBackButton = true, position) {
Expand Down Expand Up @@ -109,7 +124,8 @@ class DrawerView extends Backbone.View {
return (this._isVisible && this._isCustomViewVisible === false);
}

showDrawer(emptyDrawer, position = null) {
async showDrawer(emptyDrawer, position = null) {
shadow.show();
this.setDrawerPosition(position);
this.$el
.removeClass('u-display-none')
Expand All @@ -122,9 +138,6 @@ class DrawerView extends Backbone.View {
this._isVisible = true;
}

// Sets tab index to 0 for all tabbable elements in Drawer
this.$('a, button, input, select, textarea').attr('tabindex', 0);

if (emptyDrawer) {
this.$('.drawer__back').addClass('u-display-none');
this._isCustomViewVisible = false;
Expand All @@ -148,34 +161,15 @@ class DrawerView extends Backbone.View {
Adapt.trigger('drawer:openedCustomView');
}

$('.js-shadow').removeClass('u-display-none');
$('.js-drawer-holder').scrollTop(0);
const direction = {};
direction[this.drawerAnimationDir] = 0;

const complete = () => {
this.addShadowEvent();
$('.js-nav-drawer-btn').attr('aria-expanded', true);
Adapt.trigger('drawer:opened');
// focus on first tabbable element in drawer
a11y.focusFirst(this.$el, { defer: true });
};
$('.js-nav-drawer-btn').attr('aria-expanded', true);
Adapt.trigger('drawer:opened');

this.$el.addClass('anim-open-before');
await transitionNextFrame();
this.$el.addClass('anim-open-after');
await transitionsEnded(this.$el);

// delay drawer animation until after background fadeout animation is complete
if (this.disableAnimation) {
this.$el.css(direction);
complete();
} else {
const easing = Adapt.config.get('_drawer')?._showEasing || 'easeOutQuart';
this.$el.velocity(direction, this.drawerDuration, easing);

$('.js-shadow').velocity({ opacity: 1 }, {
duration: this.drawerDuration,
begin: () => {
complete();
}
});
}
}

emptyDrawer() {
Expand All @@ -191,56 +185,34 @@ class DrawerView extends Backbone.View {
this.collection.forEach(model => new DrawerItemView({ model }));
}

hideDrawer($toElement) {
async hideDrawer($toElement) {
if (!this._isVisible) return;
this._useMenuPosition = false;
const direction = {};
a11y.popupClosed($toElement);
this._isVisible = false;
a11y.scrollEnable('body');
direction[this.drawerAnimationDir] = -this.$el.width();

const complete = () => {
this.$el
.removeAttr('style')
.addClass('u-display-none')
.attr('aria-hidden', 'true')
.attr('aria-expanded', 'false');
this.$('.js-drawer-holder').removeAttr('role');
this._customView = null;
$('.js-nav-drawer-btn').attr('aria-expanded', false);
Adapt.trigger('drawer:closed');
this.setDrawerPosition(this._globalDrawerPosition);
};

if (this.disableAnimation) {
this.$el.css(direction);
$('.js-shadow').addClass('u-display-none');
complete();
} else {
const easing = Adapt.config.get('_drawer')?._hideEasing || 'easeInQuart';
this.$el.velocity(direction, this.drawerDuration, easing, () => {
complete();
});

$('.js-shadow').velocity({ opacity: 0 }, {
duration: this.drawerDuration,
complete() {
$('.js-shadow').addClass('u-display-none');
}
});
}

this._isCustomViewVisible = false;
this.removeShadowEvent();
}
shadow.hide();

addShadowEvent() {
$('.js-shadow').one('click touchstart', () => this.hideDrawer());
}
this.$el.addClass('anim-close-before');
await transitionNextFrame();
this.$el.addClass('anim-close-after');
await transitionsEnded(this.$el);

removeShadowEvent() {
$('.js-shadow').off('click touchstart');
this.$el.removeClass('anim-open-before anim-open-after anim-close-before anim-close-after');

a11y.popupClosed($toElement);
this._isVisible = false;
a11y.scrollEnable('body');

this.$el
.removeAttr('style')
.addClass('u-display-none')
.attr('aria-hidden', 'true')
.attr('aria-expanded', 'false');
this.$('.js-drawer-holder').removeAttr('role');
this._customView = null;
$('.js-nav-drawer-btn').attr('aria-expanded', false);
Adapt.trigger('drawer:closed');
this.setDrawerPosition(this._globalDrawerPosition);
}

remove() {
Expand All @@ -249,7 +221,6 @@ class DrawerView extends Backbone.View {
$(window).off('keyup', this.onKeyUp);
Adapt.trigger('drawer:empty');
this.collection.reset();
$('.js-shadow').remove();
}

}
Expand Down
Loading

0 comments on commit a65700b

Please sign in to comment.