mirror of https://github.com/nextcloud/photos
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:
commit
901627f294
|
@ -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')
|
||||
|
|
|
@ -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.
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.
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.
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.
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.
|
@ -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();
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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] })
|
||||
},
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue