Merge pull request #2256 from nextcloud/fix/a11y/focus-trap-add-to-album

fix: Fix focus loop on tab navigation in Add to album modal
This commit is contained in:
Pytal 2024-01-30 15:14:00 -08:00 committed by GitHub
commit 901627f294
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 621 additions and 989 deletions

View File

@ -57,7 +57,7 @@ export function addFilesToAlbumFromAlbum(albumName: string, itemsIndex: number[]
cy.intercept({ times: 1, method: 'SEARCH', url: '**/dav/' }).as('search')
cy.get('[aria-label="Add photos to this album"]').click()
cy.wait('@search')
cy.get('.file-picker__file-list').within(() => {
cy.get('.photos-picker__file-list').within(() => {
selectMedia(itemsIndex)
})
cy.intercept({ times: itemsIndex.length, method: 'COPY', url: '**/dav/files/**' }).as('copy')
@ -68,11 +68,11 @@ export function addFilesToAlbumFromAlbum(albumName: string, itemsIndex: number[]
}
export function addFilesToAlbumFromAlbumFromHeader(albumName: string, itemsIndex: number[]) {
cy.contains('Add').click()
cy.contains('New').click()
cy.intercept({ times: 1, method: 'SEARCH', url: '**/dav/' }).as('search')
cy.contains('Add photos to this album').click()
cy.wait('@search')
cy.get('.file-picker__file-list').within(() => {
cy.get('.photos-picker__file-list').within(() => {
selectMedia(itemsIndex)
})
cy.intercept({ times: 1, method: 'PROPFIND', url: `**/dav/photos/**/albums/${albumName}` }).as('propFind')

View File

@ -40,7 +40,7 @@ export function addFilesToSharedAlbumFromSharedAlbumFromHeader(albumName: string
cy.intercept({ times: 1, method: 'SEARCH', url: '**/dav/' }).as('search')
cy.contains('Add').click()
cy.wait('@search')
cy.get('.file-picker__file-list').within(() => {
cy.get('.photos-picker__file-list').within(() => {
selectMedia(itemsIndex)
})
cy.intercept({ times: itemsIndex.length, method: 'COPY', url: '**/dav/files/**' }).as('copy')
@ -54,7 +54,7 @@ export function addFilesToSharedAlbumFromAlbum(albumName: string, itemsIndex: nu
cy.intercept({ times: 1, method: 'SEARCH', url: '**/dav/' }).as('search')
cy.get('[aria-label="Add photos to this album"]').click()
cy.wait('@search')
cy.get('.file-picker__file-list').within(() => {
cy.get('.photos-picker__file-list').within(() => {
selectMedia(itemsIndex)
})
cy.intercept({ times: itemsIndex.length, method: 'COPY', url: '**/dav/files/**' }).as('copy')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -30,7 +30,6 @@ use OCA\Photos\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Constants;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
@ -110,7 +109,7 @@ class AlbumsController extends Controller {
'mime' => $node->getMimetype(),
'size' => $node->getSize(),
'type' => $node->getType(),
'permissions' => $this->formatPermissions($node->getPermissions()),
'permissions' => $node->getPermissions(),
'hasPreview' => $this->previewManager->isAvailable($node),
];
}
@ -118,28 +117,6 @@ class AlbumsController extends Controller {
return $result;
}
private function formatPermissions(int $permissions): string {
$strPermissions = '';
if ($permissions) {
if ($permissions & Constants::PERMISSION_CREATE) {
$strPermissions .= 'CK';
}
if ($permissions & Constants::PERMISSION_READ) {
$strPermissions .= 'G';
}
if ($permissions & Constants::PERMISSION_UPDATE) {
$strPermissions .= 'W';
}
if ($permissions & Constants::PERMISSION_DELETE) {
$strPermissions .= 'D';
}
if ($permissions & Constants::PERMISSION_SHARE) {
$strPermissions .= 'R';
}
}
return $strPermissions;
}
private function scanCurrentFolder(Folder $folder, bool $shared): iterable {
$nodes = $folder->getDirectoryListing();

1430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@
"@nextcloud/axios": "^2.1.0",
"@nextcloud/dialogs": "^4.1.0",
"@nextcloud/event-bus": "^3.1.0",
"@nextcloud/files": "^3.0.0-beta.13",
"@nextcloud/files": "^3.1.0",
"@nextcloud/initial-state": "^2.1.0",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/logger": "^2.5.0",
@ -52,7 +52,7 @@
"@nextcloud/paths": "^2.1.0",
"@nextcloud/router": "^2.0.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/upload": "^1.0.0-beta.8",
"@nextcloud/upload": "^1.0.4",
"@nextcloud/vue": "^8.5.0",
"camelcase": "^7.0.0",
"debounce": "^1.2.1",

View File

@ -227,7 +227,9 @@ export default {
box-sizing: border-box;
// Selection border.
&.selected, &:focus-within {
&.selected,
&:focus-within,
&:has(:focus) {
&::after {
position: absolute;
top: 0;
@ -240,6 +242,9 @@ export default {
outline-offset: -4px;
pointer-events: none;
}
.selection-checkbox {
opacity: 1;
}
}
.file {

View File

@ -20,22 +20,23 @@
-
-->
<template>
<div class="file-picker">
<div class="file-picker__content">
<nav class="file-picker__navigation" :class="{'file-picker__navigation--placeholder': monthsList.length === 0}">
<div class="photos-picker">
<div class="photos-picker__content">
<nav class="photos-picker__navigation" :class="{'photos-picker__navigation--placeholder': monthsList.length === 0}">
<ul>
<li v-for="month in monthsList"
:key="month"
class="file-picker__navigation__month"
:class="{selected: targetMonth === month}"
@click="targetMonth = month">
{{ month | dateMonthAndYear }}
class="photos-picker__navigation__month"
:class="{selected: targetMonth === month}">
<NcButton type="tertiary" :aria-label="t('photos', 'Jump to {date}', {date: dateMonthAndYear(month)})" @click="targetMonth = month">
{{ month | dateMonthAndYear }}
</NcButton>
</li>
</ul>
</nav>
<FilesListViewer class="file-picker__file-list"
:class="{'file-picker__file-list--placeholder': monthsList.length === 0}"
<FilesListViewer class="photos-picker__file-list"
:class="{'photos-picker__file-list--placeholder': monthsList.length === 0}"
:file-ids-by-section="fileIdsByMonth"
:empty-message="t('photos', 'There are no photos or videos yet!')"
:sections="monthsList"
@ -43,10 +44,11 @@
:base-height="100"
:section-header-height="50"
:scroll-to-section="targetMonth"
@need-content="getFiles">
@need-content="getFiles"
@focusout.native="onFocusOut">
<template slot-scope="{file, height, isHeader, distance}">
<h3 v-if="isHeader"
:id="`file-picker-section-header-${file.id}`"
:id="`photos-picker-section-header-${file.id}`"
:style="{ height: `${height}px`}"
class="section-header">
{{ file.id | dateMonthAndYear }}
@ -62,10 +64,10 @@
</FilesListViewer>
</div>
<div class="file-picker__actions">
<div class="photos-picker__actions">
<UploadPicker :accept="allowedMimes"
:context="uploadContext"
:destination="photosLocation"
:destination="photosLocationFolder"
:multiple="true"
@uploaded="refreshFiles" />
<NcButton type="primary" :disabled="loading || selectedFileIds.length === 0" @click="emitPickedEvent">
@ -81,7 +83,7 @@
<script>
import { mapGetters } from 'vuex'
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
import { NcButton, NcLoadingIcon, useIsMobile } from '@nextcloud/vue'
import { UploadPicker } from '@nextcloud/upload'
import moment from '@nextcloud/moment'
@ -96,6 +98,19 @@ import FilesByMonthMixin from '../mixins/FilesByMonthMixin.js'
import UserConfig from '../mixins/UserConfig.js'
import allowedMimes from '../services/AllowedMimes.js'
const isMobile = useIsMobile()
/**
* @param {string} date - In the following format: YYYYMM
*/
function dateMonthAndYear(date) {
if (isMobile.value) {
return moment(date, 'YYYYMM').format('MMM YYYY')
} else {
return moment(date, 'YYYYMM').format('MMMM YYYY')
}
}
export default {
name: 'FilesPicker',
@ -113,7 +128,8 @@ export default {
* @param {string} date - In the following format: YYYYMM
*/
dateMonthAndYear(date) {
return moment(date, 'YYYYMM').format('MMMM YYYY')
return dateMonthAndYear(date)
},
},
mixins: [
@ -168,6 +184,15 @@ export default {
},
methods: {
/**
* @param {FocusEvent} event The focus event
*/
onFocusOut(event) {
if (event.relatedTarget === null) { // Focus escaping to body
event.target.focus({ preventScroll: true })
}
},
getFiles() {
this.fetchFiles('', {}, this.blacklistIds)
},
@ -179,12 +204,18 @@ export default {
emitPickedEvent() {
this.$emit('files-picked', this.selectedFileIds)
},
/**
* @param {string} date - In the following format: YYYYMM
*/
dateMonthAndYear(date) {
return dateMonthAndYear(date)
},
},
}
</script>
<style lang="scss" scoped>
.file-picker {
.photos-picker {
display: flex;
flex-direction: column;
padding: 12px;
@ -194,14 +225,14 @@ export default {
align-items: flex-start;
flex-grow: 1;
height: 500px;
padding: 0 2px;
}
&__navigation {
flex-basis: 200px;
overflow: scroll;
margin-right: 8px;
padding-right: 8px;
height: 100%;
padding: 0 2px;
@media only screen and (max-width: 1200px) {
flex-basis: 100px;
@ -213,24 +244,7 @@ export default {
}
&__month {
font-weight: bold;
font-size: 16px;
border-radius: var(--border-radius-pill);
padding: 8px 16px;
margin: 4px 0;
cursor: pointer;
@media only screen and (max-width: 1200px) {
text-align: center;
}
&:hover {
background: var(--color-background-dark);
}
&.selected {
background: var(--color-primary-element-lighter);
}
}
}
@ -238,6 +252,7 @@ export default {
flex-grow: 1;
min-width: 0;
height: 100%;
padding: 0 4px;
&--placeholder {
background: var(--color-primary-element-light);

View File

@ -22,8 +22,10 @@
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router'
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import axios from '@nextcloud/axios'
import { joinPaths } from '@nextcloud/paths'
const eventName = 'photos:user-config-changed'
@ -36,11 +38,15 @@ export default {
? croppedLayoutLocalStorage === 'true'
: loadState('photos', 'croppedLayout', 'false') === 'true',
photosLocation: loadState('photos', 'photosLocation', ''),
photosLocationFolder: null,
}
},
created() {
async created() {
subscribe(eventName, this.updateLocalSetting)
const davClient = davGetClient()
const stat = await davClient.stat(joinPaths(davRootPath, this.photosLocation), { details: true, data: davGetDefaultPropfind() })
this.photosLocationFolder = davResultToNode(stat.data)
},
beforeDestroy() {

View File

@ -56,7 +56,7 @@
<UploadPicker v-if="album.nbItems !== 0"
:accept="allowedMimes"
:context="uploadContext"
:destination="album.basename"
:destination="albumAsFolder"
:root="uploadContext.root"
:multiple="true"
@uploaded="onUpload" />
@ -167,7 +167,7 @@
<script>
import { mapActions } from 'vuex'
import { addNewFileMenuEntry, removeNewFileMenuEntry } from '@nextcloud/files'
import { Folder, addNewFileMenuEntry, removeNewFileMenuEntry } from '@nextcloud/files'
import { getCurrentUser } from '@nextcloud/auth'
import { NcActions, NcActionButton, NcButton, NcModal, NcEmptyContent, NcActionSeparator, NcLoadingIcon, isMobile } from '@nextcloud/vue'
import { UploadPicker, getUploader } from '@nextcloud/upload'
@ -255,11 +255,11 @@ export default {
uploader: getUploader(),
/** @type {import('@nextcloud/files').Entry} */
newFileMenuEntry: {
id: 'album-add',
displayName: t('photos', 'Add photos to this album'),
templateName: '',
if: (context) => context.route === this.$route.name,
enabled: (destination) => destination.basename === this.$route.params.albumName,
/** Existing icon css class */
iconSvgInline: PlusSvg,
/** Function to be run after creation */
@ -312,6 +312,14 @@ export default {
albumFileName() {
return this.$store.getters.getAlbumName(this.albumName)
},
albumAsFolder() {
return new Folder({
...this.album,
owner: getCurrentUser()?.uid ?? '',
source: this.album?.source ?? '',
})
},
},
async mounted() {

View File

@ -43,7 +43,7 @@
:root-title="rootTitle"
@refresh="onRefresh">
<UploadPicker :accept="allowedMimes"
:destination="path"
:destination="folderAsFolder"
:multiple="true"
@uploaded="onUpload" />
</HeaderNavigation>
@ -69,7 +69,8 @@
<script>
import { mapGetters } from 'vuex'
import { UploadPicker, getUploader } from '@nextcloud/upload'
import { Upload, UploadPicker, getUploader } from '@nextcloud/upload'
import { Folder as NcFolder } from '@nextcloud/files'
import { NcEmptyContent } from '@nextcloud/vue'
import VirtualGrid from 'vue-virtual-grid'
@ -142,6 +143,12 @@ export default {
folder() {
return this.files[this.folderId]
},
folderAsFolder() {
return new NcFolder({
...this.folder,
source: decodeURI(this.folder.source),
})
},
folderContent() {
return this.folders[this.folderId]
},
@ -273,15 +280,13 @@ export default {
/**
* Fetch file Info and add them into the store
*
* @param {Upload[]} uploads the newly uploaded files
* @param {Upload} upload the newly uploaded files
*/
onUpload(uploads) {
uploads.forEach(async upload => {
const relPath = upload.path.split(prefixPath).pop()
const file = await getFileInfo(relPath)
this.$store.dispatch('appendFiles', [file])
this.$store.dispatch('addFilesToFolder', { fileid: this.folderId, files: [file] })
})
async onUpload(upload) {
const relPath = upload.source.split(prefixPath).pop()
const file = await getFileInfo(relPath)
this.$store.dispatch('appendFiles', [file])
this.$store.dispatch('addFilesToFolder', { fileid: this.folderId, files: [file] })
},
},