Merge pull request #32616 from nextcloud/enh/a11y-keyboard-filepicker

Augment file picker modal accessibility
This commit is contained in:
Vincent Petry 2022-06-09 13:20:19 +02:00 committed by GitHub
commit dac93fe048
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 130 additions and 90 deletions

View file

@ -54,7 +54,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
}
:focus-visible {
box-shadow: 0 0 0 2px var(--color-primary);
box-shadow: inset 0 0 0 2px var(--color-primary);
outline: none;
}
@ -711,7 +711,6 @@ code {
position: relative;
display: flex;
flex-direction: column;
/* Grid view toggle */
}
#oc-dialog-filepicker-content .dirtree {
flex-wrap: wrap;
@ -733,7 +732,7 @@ code {
content: ">";
padding: 3px;
}
#oc-dialog-filepicker-content #picker-view-toggle {
#oc-dialog-filepicker-content #picker-showgridview {
position: absolute;
background-color: transparent;
border: none;
@ -743,10 +742,8 @@ code {
right: 0;
top: 0;
}
#oc-dialog-filepicker-content #picker-view-toggle:hover, #oc-dialog-filepicker-content #picker-view-toggle:focus {
opacity: 1;
}
#oc-dialog-filepicker-content #picker-showgridview:focus + #picker-view-toggle {
#oc-dialog-filepicker-content #picker-showgridview:hover, #oc-dialog-filepicker-content #picker-showgridview:active, #oc-dialog-filepicker-content #picker-showgridview:focus {
box-shadow: 0 0 0 2px var(--color-primary);
opacity: 1;
}
#oc-dialog-filepicker-content .actions.creatable {

File diff suppressed because one or more lines are too long

View file

@ -53,7 +53,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
}
:focus-visible {
box-shadow: 0 0 0 2px var(--color-primary);
box-shadow: inset 0 0 0 2px var(--color-primary);
outline: none;
}
@ -710,7 +710,6 @@ code {
position: relative;
display: flex;
flex-direction: column;
/* Grid view toggle */
}
#oc-dialog-filepicker-content .dirtree {
flex-wrap: wrap;
@ -732,7 +731,7 @@ code {
content: ">";
padding: 3px;
}
#oc-dialog-filepicker-content #picker-view-toggle {
#oc-dialog-filepicker-content #picker-showgridview {
position: absolute;
background-color: transparent;
border: none;
@ -742,10 +741,8 @@ code {
right: 0;
top: 0;
}
#oc-dialog-filepicker-content #picker-view-toggle:hover, #oc-dialog-filepicker-content #picker-view-toggle:focus {
opacity: 1;
}
#oc-dialog-filepicker-content #picker-showgridview:focus + #picker-view-toggle {
#oc-dialog-filepicker-content #picker-showgridview:hover, #oc-dialog-filepicker-content #picker-showgridview:active, #oc-dialog-filepicker-content #picker-showgridview:focus {
box-shadow: 0 0 0 2px var(--color-primary);
opacity: 1;
}
#oc-dialog-filepicker-content .actions.creatable {

File diff suppressed because one or more lines are too long

View file

@ -34,7 +34,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
}
:focus-visible {
box-shadow: 0 0 0 2px var(--color-primary);
box-shadow: inset 0 0 0 2px var(--color-primary);
outline: none;
}
@ -760,8 +760,7 @@ code {
}
}
/* Grid view toggle */
#picker-view-toggle {
#picker-showgridview {
position: absolute;
background-color: transparent;
border: none;
@ -772,16 +771,13 @@ code {
top: 0;
&:hover,
&:active,
&:focus {
box-shadow: 0 0 0 2px var(--color-primary);
opacity: 1;
}
}
// keyboard focus
#picker-showgridview:focus + #picker-view-toggle {
opacity: 1;
}
.actions.creatable {
flex-wrap: wrap;
padding: 0px;

View file

@ -49,6 +49,7 @@ import $ from 'jquery'
import OC from './index'
import OCA from '../OCA/index'
import { isA11yActivation } from '../Util/a11y'
/**
* this class to ease the usage of jquery dialogs
@ -308,14 +309,14 @@ const Dialogs = {
multiselect = false
}
self.$filePicker.find('#picker-view-toggle').remove()
self.$filePicker.find('#picker-filestable').removeClass('view-grid')
$('body').append(self.$filePicker)
self.$showGridView = $('input#picker-showgridview')
self.$showGridView.on('change', _.bind(self._onGridviewChange, self))
$('body').prepend(self.$filePicker)
self.$showGridView = $('button#picker-showgridview')
self.$showGridView.on('click keydown', function(event) {
if (isA11yActivation(event)) {
self._onGridviewChange()
}
})
self._getGridSettings()
var newButton = self.$filePicker.find('.actions.creatable .button-add')
@ -323,7 +324,7 @@ const Dialogs = {
newButton.hide()
}
newButton.on('focus', function() {
self.$filePicker.ocdialog('setEnterCallback', function() {
self.$filePicker.ocdialog('setEnterCallback', function(event) {
event.stopImmediatePropagation()
event.preventDefault()
newButton.click()
@ -336,7 +337,7 @@ const Dialogs = {
OC.registerMenu(newButton, self.$filePicker.find('.menu'), function() {
$input.tooltip('hide')
$input.focus()
self.$filePicker.ocdialog('setEnterCallback', function() {
self.$filePicker.ocdialog('setEnterCallback', function(event) {
event.stopImmediatePropagation()
event.preventDefault()
self.$filePicker.submit()
@ -351,6 +352,13 @@ const Dialogs = {
var $form = self.$filePicker.find('.filenameform')
var $input = $form.find('input[type=\'text\']')
var $submit = $form.find('input[type=\'submit\']')
$input.on('keydown', function(event) {
if (isA11yActivation(event)) {
event.stopImmediatePropagation()
event.preventDefault()
$form.submit()
}
})
$submit.on('click', function(event) {
event.stopImmediatePropagation()
event.preventDefault()
@ -427,13 +435,6 @@ const Dialogs = {
$input.val(newText)
}
})
$input.keypress(function(event) {
if (event.keyCode === 13 || event.which === 13) {
event.stopImmediatePropagation()
event.preventDefault()
$form.submit()
}
})
$input.on('input', function(event) {
$input.tooltip('hide')
})
@ -443,17 +444,23 @@ const Dialogs = {
self.$filelist = self.$filePicker.find('.filelist tbody')
self.$filelistContainer = self.$filePicker.find('.filelist-container')
self.$dirTree = self.$filePicker.find('.dirtree')
self.$dirTree.on('click', 'div:not(:last-child)', self, function(event) {
self._handleTreeListSelect(event, type)
self.$dirTree.on('click keydown', 'div:not(:last-child)', self, function(event) {
if (isA11yActivation(event)) {
self._handleTreeListSelect(event, type)
}
})
self.$filelist.on('click', 'tr', function(event) {
self._handlePickerClick(event, $(this), type)
self.$filelist.on('click keydown', 'tr', function(event) {
if (isA11yActivation(event)) {
self._handlePickerClick(event, $(this), type)
}
})
self.$fileListHeader.on('click', 'a', function(event) {
var dir = self.$filePicker.data('path')
self.filepicker.sortField = $(event.currentTarget).data('sort')
self.filepicker.sortOrder = self.filepicker.sortOrder === 'asc' ? 'desc' : 'asc'
self._fillFilePicker(dir)
self.$fileListHeader.on('click keydown', 'a', function(event) {
if (isA11yActivation(event)) {
var dir = self.$filePicker.data('path')
self.filepicker.sortField = $(event.currentTarget).data('sort')
self.filepicker.sortOrder = self.filepicker.sortOrder === 'asc' ? 'desc' : 'asc'
self._fillFilePicker(dir)
}
})
self._fillFilePicker(path)
})
@ -1033,27 +1040,24 @@ const Dialogs = {
},
// get the gridview setting and set the input accordingly
_getGridSettings: function() {
var self = this
const self = this
$.get(OC.generateUrl('/apps/files/api/v1/showgridview'), function(response) {
self.$showGridView.get(0).checked = response.gridview
self.$showGridView.next('#picker-view-toggle')
self.$showGridView
.removeClass('icon-toggle-filelist icon-toggle-pictures')
.addClass(response.gridview ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
$('.list-container').toggleClass('view-grid', response.gridview)
})
},
_onGridviewChange: function() {
var show = this.$showGridView.is(':checked')
const isGridView = this.$showGridView.hasClass('icon-toggle-filelist')
// only save state if user is logged in
if (OC.currentUser) {
$.post(OC.generateUrl('/apps/files/api/v1/showgridview'), {
show: show
})
$.post(OC.generateUrl('/apps/files/api/v1/showgridview'), { show: !isGridView })
}
this.$showGridView.next('#picker-view-toggle')
this.$showGridView
.removeClass('icon-toggle-filelist icon-toggle-pictures')
.addClass(show ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
$('.list-container').toggleClass('view-grid', show)
.addClass(isGridView ? 'icon-toggle-pictures' : 'icon-toggle-filelist')
$('.list-container').toggleClass('view-grid', !isGridView)
},
_getFilePickerTemplate: function() {
var defer = $.Deferred()
@ -1272,7 +1276,7 @@ const Dialogs = {
var dir
var path = this.$filePicker.data('path')
var $template = $('<div data-dir="{dir}"><a>{name}</a></div>').addClass('crumb')
var $template = $('<div data-dir="{dir}" tabindex="0"><a>{name}</a></div>').addClass('crumb')
if (path) {
var paths = path.split('/')
$.each(paths, function(index, dir) {

38
core/src/Util/a11y.js Normal file
View file

@ -0,0 +1,38 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* Return whether the DOM event is an accessible mouse or keyboard element activation
*
* @param {Event} event DOM event
*
* @return {boolean}
*/
export const isA11yActivation = (event) => {
if (event.type === 'click') {
return true
}
if (event.type === 'keydown' && event.key === 'Enter') {
return true
}
return false
}

View file

@ -55,6 +55,7 @@
padding: 25px;
background: var(--icon-close-dark) no-repeat center;
opacity: .5;
border-radius: var(--border-radius-pill);
&:hover,
&:focus,

View file

@ -24,6 +24,7 @@
*/
import $ from 'jquery'
import { isA11yActivation } from '../Util/a11y'
$.widget('oc.ocdialog', {
options: {
@ -56,6 +57,21 @@ $.widget('oc.ocdialog', {
this.$dialog.append(this.element.detach())
this.element.removeAttr('title').addClass('oc-dialog-content').appendTo(this.$dialog)
// Activate the primary button on enter if there is a single input
if (self.element.find('input').length === 1) {
const $input = self.element.find('input')
$input.on('keydown', function(event) {
if (isA11yActivation(event)) {
if (self.$buttonrow) {
const $button = self.$buttonrow.find('button.primary')
if ($button && !$button.prop('disabled')) {
$button.click()
}
}
}
})
}
this.$dialog.css({
display: 'inline-block',
position: 'fixed',
@ -92,18 +108,6 @@ $.widget('oc.ocdialog', {
event.preventDefault()
return false
}
// If no button is selected we trigger the primary
if (
self.$buttonrow
&& self.$buttonrow.find($(event.target)).length === 0
) {
const $button = self.$buttonrow.find('button.primary')
if ($button && !$button.prop('disabled')) {
$button.trigger('click')
}
} else if (self.$buttonrow) {
$(event.target).trigger('click')
}
return false
}
})
@ -153,8 +157,10 @@ $.widget('oc.ocdialog', {
self.$defaultButton = $button
}
self.$buttonrow.append($button)
$button.click(function() {
val.click.apply(self.element[0], arguments)
$button.on('click keydown', function(event) {
if (isA11yActivation(event)) {
val.click.apply(self.element[0], arguments)
}
})
})
this.$buttonrow.find('button')
@ -171,11 +177,13 @@ $.widget('oc.ocdialog', {
break
case 'closeButton':
if (value) {
const $closeButton = $('<a class="oc-dialog-close"></a>')
const $closeButton = $('<a class="oc-dialog-close" tabindex="0"></a>')
this.$dialog.prepend($closeButton)
$closeButton.on('click', function() {
self.options.closeCallback && self.options.closeCallback()
self.close()
$closeButton.on('click keydown', function(event) {
if (isA11yActivation(event)) {
self.options.closeCallback && self.options.closeCallback()
self.close()
}
})
} else {
this.$dialog.find('.oc-dialog-close').remove()

View file

@ -13,8 +13,7 @@
</span>
<input type="checkbox" class="hidden-visually" id="picker-showgridview" checked="checked" />
<label id="picker-view-toggle" for="picker-showgridview" class="button icon-toggle-filelist"></label>
<button id="picker-showgridview" class="icon-toggle-pictures"></button>
<div class="filelist-container">
<div class="emptycontent">
<div class="icon-folder"></div>
@ -25,26 +24,26 @@
<tr>
<th id="headerName" class="column-name">
<div id="headerName-container">
<a class="name sort columntitle" data-sort="name">
<a class="name sort columntitle" data-sort="name" tabindex="0">
<span>{nameCol}</span>
<span class="sort-indicator hidden icon-triangle-n"></span>
</a>
</div>
</th>
<th id="headerSize" class="column-size">
<a class="size sort columntitle" data-sort="size">
<a class="size sort columntitle" data-sort="size" tabindex="0">
<span>{sizeCol}</span>
<span class="sort-indicator hidden icon-triangle-n"></span></a>
</th>
<th id="headerDate" class="column-mtime">
<a id="modified" class="columntitle" data-sort="mtime">
<a id="modified" class="columntitle" data-sort="mtime" tabindex="0">
<span>{modifiedCol}</span>
<span class="sort-indicator hidden icon-triangle-n"></span></a>
</th>
</tr>
</thead>
<tbody>
<tr data-entryname="{filename}" data-type="{type}">
<tr data-entryname="{filename}" data-type="{type}" tabindex="0">
<td class="filename"
style="background-image:url({icon})">
<span class="filename-parts">

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long