diff options
Diffstat (limited to 'client/lib')
-rw-r--r-- | client/lib/accessibility.js | 41 | ||||
-rw-r--r-- | client/lib/filter.js | 4 | ||||
-rw-r--r-- | client/lib/modal.js | 4 | ||||
-rw-r--r-- | client/lib/multiSelection.js | 9 | ||||
-rw-r--r-- | client/lib/popup.js | 4 | ||||
-rw-r--r-- | client/lib/textComplete.js | 30 |
6 files changed, 82 insertions, 10 deletions
diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js new file mode 100644 index 00000000..52b771d4 --- /dev/null +++ b/client/lib/accessibility.js @@ -0,0 +1,41 @@ +// In this file we define a set of DOM transformations that are specifically +// intended for blind screen readers. +// +// See https://github.com/wekan/wekan/issues/337 for the general accessibility +// considerations. + +// Without an href, links are non-keyboard-focusable and are not presented on +// blind screen readers. We default to the empty anchor `#` href. +function enforceHref(attributes) { + if (!_.has(attributes, 'href')) { + attributes.href = '#'; + } + return attributes; +} + +// `title` is inconsistently used on the web, and is thus inconsistently +// presented by screen readers. `aria-label`, on the other hand, is specific to +// accessibility and is presented in ways that title shouldn't be. +function copyTitleInAriaLabel(attributes) { + if (!_.has(attributes, 'aria-label') && _.has(attributes, 'title')) { + attributes['aria-label'] = attributes.title; + } + return attributes; +} + +// XXX Our implementation relies on overwriting Blaze virtual DOM functions, +// which is a little bit hacky -- but still reasonable with our ES6 usage. If we +// end up switching to React we will probably create lower level small +// components to handle that without overwriting any build-in function. +const { + A: superA, + I: superI, +} = HTML; + +HTML.A = (attributes, ...others) => { + return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others); +}; + +HTML.I = (attributes, ...others) => { + return superI(copyTitleInAriaLabel(attributes), ...others); +}; diff --git a/client/lib/filter.js b/client/lib/filter.js index f7baf480..74305284 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -95,7 +95,7 @@ Filter = { return {}; const filterSelector = {}; - _.forEach(this._fields, (fieldName) => { + this._fields.forEach((fieldName) => { const filter = this[fieldName]; if (filter._isActive()) filterSelector[fieldName] = filter._getMongoSelector(); @@ -116,7 +116,7 @@ Filter = { }, reset() { - _.forEach(this._fields, (fieldName) => { + this._fields.forEach((fieldName) => { const filter = this[fieldName]; filter.reset(); }); diff --git a/client/lib/modal.js b/client/lib/modal.js index 5b3392b2..e6301cb5 100644 --- a/client/lib/modal.js +++ b/client/lib/modal.js @@ -21,9 +21,9 @@ window.Modal = new class { } } - open(modalName, options) { + open(modalName, { onCloseGoTo = ''} = {}) { this._currentModal.set(modalName); - this._onCloseGoTo = options && options.onCloseGoTo || ''; + this._onCloseGoTo = onCloseGoTo; } }; diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js index c2bb2bbc..eeb2015d 100644 --- a/client/lib/multiSelection.js +++ b/client/lib/multiSelection.js @@ -119,12 +119,13 @@ MultiSelection = { } }, - toggle(cardIds, options) { + toggle(cardIds, options = {}) { cardIds = _.isString(cardIds) ? [cardIds] : cardIds; - options = _.extend({ + options = { add: true, remove: true, - }, options || {}); + ...options, + }; if (!this.isActive()) { this.reset(); @@ -133,7 +134,7 @@ MultiSelection = { const selectedCards = this._selectedCards.get(); - _.each(cardIds, (cardId) => { + cardIds.forEach((cardId) => { const indexOfCard = selectedCards.indexOf(cardId); if (options.remove && indexOfCard > -1) diff --git a/client/lib/popup.js b/client/lib/popup.js index 3c39af29..7418d938 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -91,7 +91,7 @@ window.Popup = new class { if (!self.isOpen()) { self.current = Blaze.renderWithData(self.template, () => { self._dep.depend(); - return _.extend(self._getTopStack(), { stack: self._stack }); + return { ...self._getTopStack(), stack: self._stack }; }, document.body); } else { @@ -191,7 +191,7 @@ window.Popup = new class { // We close a potential opened popup on any left click on the document, or go // one step back by pressing escape. const escapeActions = ['back', 'close']; -_.each(escapeActions, (actionName) => { +escapeActions.forEach((actionName) => { EscapeActions.register(`popup-${actionName}`, () => Popup[actionName](), () => Popup.isOpen(), diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js new file mode 100644 index 00000000..e50d7cbc --- /dev/null +++ b/client/lib/textComplete.js @@ -0,0 +1,30 @@ +// We “inherit” the jquery-textcomplete plugin to integrate with our +// EscapeActions system. You should always use `escapeableTextComplete` instead +// of the vanilla `textcomplete`. +let dropdownMenuIsOpened = false; + +$.fn.escapeableTextComplete = function(...args) { + this.textcomplete(...args); + + // Since commit d474017 jquery-textComplete automatically closes a potential + // opened dropdown menu when the user press Escape. This behavior conflicts + // with our EscapeActions system, but it's too complicated and hacky to + // monkey-pach textComplete to disable it -- I tried. Instead we listen to + // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown + // is opened (and rely on textComplete to execute the actual action). + this.on({ + 'textComplete:show'() { + dropdownMenuIsOpened = true; + }, + 'textComplete:hide'() { + Tracker.afterFlush(() => { + dropdownMenuIsOpened = false; + }); + }, + }); +}; + +EscapeActions.register('textcomplete', + () => {}, + () => dropdownMenuIsOpened +); |