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

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
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
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