mirror of
https://github.com/nextcloud/server.git
synced 2026-04-20 22:00:39 -04:00
Merge pull request #32616 from nextcloud/enh/a11y-keyboard-filepicker
Augment file picker modal accessibility
This commit is contained in:
commit
dac93fe048
14 changed files with 130 additions and 90 deletions
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
38
core/src/Util/a11y.js
Normal 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
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
padding: 25px;
|
||||
background: var(--icon-close-dark) no-repeat center;
|
||||
opacity: .5;
|
||||
border-radius: var(--border-radius-pill);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
|
|
|
|||
44
core/src/jquery/ocdialog.js
vendored
44
core/src/jquery/ocdialog.js
vendored
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue