Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: CSS to animate popups, dialog for accessibility (fixes #600) #601

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
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
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;
141 changes: 57 additions & 84 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 @@ -30,17 +38,21 @@ class DrawerView extends Backbone.View {
}

initialize() {
_.bindAll(this, 'onShadowClicked');
this._isVisible = false;
this.disableAnimation = Adapt.config.has('_disableAnimation') ? Adapt.config.get('_disableAnimation') : false;
this.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;
const drawerDuration = Adapt.config.get('_drawer')?._duration ?? 400;
document.documentElement.style.setProperty('--adapt-drawer-duration', `${drawerDuration}ms`);
this.setupEventListeners();
this.render();
}

setupEventListeners() {
this.onKeyUp = this.onKeyUp.bind(this);
$(window).on('keyup', this.onKeyUp);
this.el.addEventListener('click', this.onShadowClicked, { capture: true });
}

onKeyUp(event) {
Expand All @@ -49,18 +61,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 +92,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 +126,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 +140,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 +163,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-show-before');
await transitionNextFrame();
this.$el.addClass('anim-show-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 +187,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-hide-before');
await transitionNextFrame();
this.$el.addClass('anim-hide-after');
await transitionsEnded(this.$el);

removeShadowEvent() {
$('.js-shadow').off('click touchstart');
this.$el.removeClass('anim-show-before anim-show-after anim-hide-before anim-hide-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 +223,6 @@ class DrawerView extends Backbone.View {
$(window).off('keyup', this.onKeyUp);
Adapt.trigger('drawer:empty');
this.collection.reset();
$('.js-shadow').remove();
}

}
Expand Down
Loading