Skip to content

Commit 8f3019c

Browse files
committed
fix(files): VirtualList rendering for scrolling calculations
Signed-off-by: skjnldsv <[email protected]>
1 parent 2d45420 commit 8f3019c

File tree

4 files changed

+105
-82
lines changed

4 files changed

+105
-82
lines changed

apps/files/src/components/FilesListVirtual.vue

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
import type { UserConfig } from '../types'
7171
import type { Node as NcNode } from '@nextcloud/files'
7272
import type { ComponentPublicInstance, PropType } from 'vue'
73-
import type { Location } from 'vue-router'
7473
7574
import { Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files'
7675
import { showError } from '@nextcloud/dialogs'
@@ -86,6 +85,7 @@ import { useFileListWidth } from '../composables/useFileListWidth.ts'
8685
import { useRouteParameters } from '../composables/useRouteParameters.ts'
8786
import { useSelectionStore } from '../store/selection.js'
8887
import { useUserConfigStore } from '../store/userconfig.ts'
88+
import logger from '../logger.ts'
8989
9090
import FileEntry from './FileEntry.vue'
9191
import FileEntryGrid from './FileEntryGrid.vue'
@@ -95,7 +95,6 @@ import FilesListTableFooter from './FilesListTableFooter.vue'
9595
import FilesListTableHeader from './FilesListTableHeader.vue'
9696
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
9797
import VirtualList from './VirtualList.vue'
98-
import logger from '../logger.ts'
9998
10099
export default defineComponent({
101100
name: 'FilesListVirtual',
@@ -230,28 +229,17 @@ export default defineComponent({
230229
watch: {
231230
// If nodes gets populated and we have a fileId,
232231
// an openFile or openDetails, we fire the appropriate actions.
233-
isEmpty(isEmpty: boolean) {
234-
if (isEmpty || !this.fileId) {
235-
return
236-
}
237-
238-
logger.debug('FilesListVirtual: nodes populated, checking for requested fileId, openFile or openDetails again', {
239-
fileId: this.fileId,
240-
openFile: this.openFile,
241-
openDetails: this.openDetails,
242-
})
243-
244-
if (this.openFile) {
245-
this.handleOpenFile(this.fileId)
246-
}
247-
248-
if (this.openDetails) {
249-
this.openSidebarForFile(this.fileId)
250-
}
251-
252-
if (this.fileId) {
253-
this.scrollToFile(this.fileId, false)
254-
}
232+
isEmpty() {
233+
this.handleOpenQueries()
234+
},
235+
fileId() {
236+
this.handleOpenQueries()
237+
},
238+
openFile() {
239+
this.handleOpenQueries()
240+
},
241+
openDetails() {
242+
this.handleOpenQueries()
255243
},
256244
},
257245
@@ -281,6 +269,33 @@ export default defineComponent({
281269
},
282270
283271
methods: {
272+
handleOpenQueries() {
273+
// If the list is empty, or we don't have a fileId,
274+
// there's nothing to be done.
275+
if (this.isEmpty || !this.fileId) {
276+
return
277+
}
278+
279+
logger.debug('FilesListVirtual: checking for requested fileId, openFile or openDetails', {
280+
nodes: this.nodes,
281+
fileId: this.fileId,
282+
openFile: this.openFile,
283+
openDetails: this.openDetails,
284+
})
285+
286+
if (this.openFile) {
287+
this.handleOpenFile(this.fileId)
288+
}
289+
290+
if (this.openDetails) {
291+
this.openSidebarForFile(this.fileId)
292+
}
293+
294+
if (this.fileId) {
295+
this.scrollToFile(this.fileId, false)
296+
}
297+
},
298+
284299
openSidebarForFile(fileId) {
285300
// Open the sidebar for the given URL fileid
286301
// iif we just loaded the app.
@@ -306,6 +321,7 @@ export default defineComponent({
306321
}
307322
308323
this.scrollToIndex = Math.max(0, index)
324+
logger.debug('Scrolling to file ' + fileId, { fileId, index })
309325
}
310326
},
311327
@@ -370,15 +386,13 @@ export default defineComponent({
370386
}
371387
// The file is either a folder or has no default action other than downloading
372388
// in this case we need to open the details instead and remove the route from the history
373-
const query = this.$route.query
374-
delete query.openfile
375-
query.opendetails = ''
376-
377389
logger.debug('Ignore `openfile` query and replacing with `opendetails` for ' + node.path, { node })
378-
await this.$router.replace({
379-
...(this.$route as Location),
380-
query,
381-
})
390+
window.OCP.Files.Router.goToRoute(
391+
null,
392+
this.$route.params,
393+
{ ...this.$route.query, openfile: undefined, opendetails: '' },
394+
true, // silent update of the URL
395+
)
382396
},
383397
384398
onDragOver(event: DragEvent) {
@@ -525,6 +539,13 @@ export default defineComponent({
525539
// Hide the table header below the overlay
526540
margin-block-start: calc(-1 * var(--row-height));
527541
}
542+
543+
// Visually hide the table when there are no files
544+
&--hidden {
545+
visibility: hidden;
546+
z-index: -1;
547+
opacity: 0;
548+
}
528549
}
529550
530551
.files-list__filters {

apps/files/src/components/VirtualList.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
<slot name="empty" />
2626
</div>
2727

28-
<table v-show="dataSources.length > 0"
29-
:aria-hidden="dataSources.length === 0"
28+
<table :aria-hidden="dataSources.length === 0"
3029
:inert="dataSources.length === 0"
3130
class="files-list__table"
32-
:class="{ 'files-list__table--with-thead-overlay': !!$scopedSlots['header-overlay'] }">
31+
:class="{
32+
'files-list__table--with-thead-overlay': !!$scopedSlots['header-overlay'],
33+
'files-list__table--hidden': dataSources.length === 0,
34+
}">
3335
<!-- Accessibility table caption for screen readers -->
3436
<caption v-if="caption" class="hidden-visually">
3537
{{ caption }}
@@ -318,7 +320,7 @@ export default defineComponent({
318320
319321
methods: {
320322
scrollTo(index: number) {
321-
if (!this.$el) {
323+
if (!this.$el || this.index === index) {
322324
return
323325
}
324326

cypress/e2e/files/FilesUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@ export function enableGridMode() {
293293
export function calculateViewportHeight(rows: number): Cypress.Chainable<number> {
294294
cy.visit('/apps/files')
295295

296+
cy.get('[data-cy-files-list]')
297+
.should('be.visible')
298+
299+
cy.get('[data-cy-files-list-tbody] tr', { timeout: 5000 })
300+
.and('be.visible')
301+
296302
return cy.get('[data-cy-files-list]')
297303
.should('be.visible')
298304
.then((filesList) => {

cypress/e2e/files/scrolling.cy.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,23 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import type { User } from '@nextcloud/cypress'
76
import { calculateViewportHeight, enableGridMode, getRowForFile } from './FilesUtils.ts'
87
import { beFullyInViewport, notBeFullyInViewport } from '../core-utils.ts'
98

10-
describe('files: Scrolling to selected file in file list', { testIsolation: true }, () => {
9+
describe('files: Scrolling to selected file in file list', () => {
1110
const fileIds = new Map<number, string>()
12-
let user: User
1311
let viewportHeight: number
1412

1513
before(() => {
16-
cy.createRandomUser().then(($user) => {
17-
user = $user
18-
19-
cy.rm(user, '/welcome.txt')
20-
for (let i = 1; i <= 10; i++) {
21-
cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`)
22-
.then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString()))
23-
}
24-
25-
cy.login(user)
26-
cy.viewport(1200, 800)
27-
// Calculate height to ensure that those 10 elements can not be rendered in one list (only 6 will fit the screen)
28-
calculateViewportHeight(6)
29-
.then((height) => { viewportHeight = height })
30-
})
14+
initFilesAndViewport(fileIds)
15+
.then((_viewportHeight) => {
16+
cy.log(`Saving viewport height to ${_viewportHeight}px`)
17+
viewportHeight = _viewportHeight
18+
})
3119
})
3220

3321
beforeEach(() => {
3422
cy.viewport(1200, viewportHeight)
35-
cy.login(user)
3623
})
3724

3825
it('Can see first file in list', () => {
@@ -123,41 +110,17 @@ describe('files: Scrolling to selected file in file list', { testIsolation: true
123110
}
124111
})
125112

126-
describe('files: Scrolling to selected file in file list (GRID MODE)', { testIsolation: true }, () => {
113+
describe('files: Scrolling to selected file in file list (GRID MODE)', () => {
127114
const fileIds = new Map<number, string>()
128-
let user: User
129115
let viewportHeight: number
130116

131117
before(() => {
132-
cy.wrap(Cypress.automation('remote:debugger:protocol', {
133-
command: 'Network.clearBrowserCache',
134-
}))
135-
136-
cy.createRandomUser().then(($user) => {
137-
user = $user
138-
139-
cy.rm(user, '/welcome.txt')
140-
for (let i = 1; i <= 12; i++) {
141-
cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`)
142-
.then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString()))
143-
}
144-
145-
// Set grid mode
146-
cy.login(user)
147-
cy.visit('/apps/files')
148-
enableGridMode()
149-
150-
// 768px width will limit the columns to 3
151-
cy.viewport(768, 800)
152-
// Calculate height to ensure that those 12 elements can not be rendered in one list (only 3 will fit the screen)
153-
calculateViewportHeight(3)
154-
.then((height) => { viewportHeight = height })
155-
})
118+
initFilesAndViewport(fileIds, true)
119+
.then((_viewportHeight) => { viewportHeight = _viewportHeight })
156120
})
157121

158122
beforeEach(() => {
159123
cy.viewport(768, viewportHeight)
160-
cy.login(user)
161124
})
162125

163126
// First row
@@ -288,3 +251,34 @@ function beOverlappedByTableHeader($el: JQuery<HTMLElement>, expected = true) {
288251
function notBeOverlappedByTableHeader($el: JQuery<HTMLElement>) {
289252
return beOverlappedByTableHeader($el, false)
290253
}
254+
255+
function initFilesAndViewport(fileIds: Map<number, string>, gridMode = false): Cypress.Chainable<number> {
256+
return cy.createRandomUser().then((user) => {
257+
cy.rm(user, '/welcome.txt')
258+
259+
// Create files with names 1.txt, 2.txt, ..., 10.txt
260+
const count = gridMode ? 12 : 10
261+
for (let i = 1; i <= count; i++) {
262+
cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`)
263+
.then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString()))
264+
}
265+
266+
cy.login(user)
267+
cy.viewport(1200, 800)
268+
269+
cy.visit('/apps/files')
270+
271+
// If grid mode is requested, enable it
272+
if (gridMode) {
273+
enableGridMode()
274+
}
275+
276+
// Calculate height to ensure that those 10 elements can not be rendered in one list (only 6 will fit the screen, 3 in grid mode)
277+
return calculateViewportHeight(gridMode ? 3 : 6)
278+
.then((height) => {
279+
// Set viewport height to the calculated height
280+
cy.log(`Setting viewport height to ${height}px`)
281+
cy.wrap(height)
282+
})
283+
})
284+
}

0 commit comments

Comments
 (0)