Merge pull request #49305 from nextcloud/refactor/files-filelist-width

refactor(files): Provide `useFileListWidth` composable
This commit is contained in:
Ferdinand Thiessen 2024-11-20 19:55:57 +01:00 committed by GitHub
commit 5a8d32fe53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 158 additions and 87 deletions

View file

@ -14,7 +14,7 @@
v-bind="section"
dir="auto"
:to="section.to"
:force-icon-text="index === 0 && filesListWidth >= 486"
:force-icon-text="index === 0 && fileListWidth >= 486"
:title="titleForSection(index, section)"
:aria-description="ariaForSection(section)"
@click.native="onClick(section.to)"
@ -46,15 +46,15 @@ import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { useNavigation } from '../composables/useNavigation'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import { useNavigation } from '../composables/useNavigation.ts'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
export default defineComponent({
@ -66,10 +66,6 @@ export default defineComponent({
NcIconSvgWrapper,
},
mixins: [
filesListWidthMixin,
],
props: {
path: {
type: String,
@ -83,6 +79,7 @@ export default defineComponent({
const pathsStore = usePathsStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
const fileListWidth = useFileListWidth()
const { currentView, views } = useNavigation()
return {
@ -93,6 +90,7 @@ export default defineComponent({
uploaderStore,
currentView,
fileListWidth,
views,
}
},
@ -129,7 +127,7 @@ export default defineComponent({
wrapUploadProgressBar(): boolean {
// if an upload is ongoing, and on small screens / mobile, then
// show the progress bar for the upload below breadcrumbs
return this.isUploadInProgress && this.filesListWidth < 512
return this.isUploadInProgress && this.fileListWidth < 512
},
// used to show the views icon for the first breadcrumb

View file

@ -36,7 +36,6 @@
<FileEntryName ref="name"
:basename="basename"
:extension="extension"
:files-list-width="filesListWidth"
:nodes="nodes"
:source="source"
@auxclick.native="execDefaultAction"
@ -47,7 +46,6 @@
<FileEntryActions v-show="!isRenamingSmallScreen"
ref="actions"
:class="`files-list__row-actions-${uniqueId}`"
:files-list-width="filesListWidth"
:loading.sync="loading"
:opened.sync="openedMenu"
:source="source" />
@ -91,6 +89,7 @@ import { formatFileSize } from '@nextcloud/files'
import moment from '@nextcloud/moment'
import { useNavigation } from '../composables/useNavigation.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
@ -135,6 +134,7 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
const filesListWidth = useFileListWidth()
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
const {
@ -152,6 +152,7 @@ export default defineComponent({
currentDir,
currentFileId,
currentView,
filesListWidth,
}
},

View file

@ -94,6 +94,7 @@ import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
import CustomElementRender from '../CustomElementRender.vue'
import { useNavigation } from '../../composables/useNavigation'
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
import logger from '../../logger.ts'
export default defineComponent({
@ -110,10 +111,6 @@ export default defineComponent({
},
props: {
filesListWidth: {
type: Number,
required: true,
},
loading: {
type: String,
required: true,
@ -135,11 +132,14 @@ export default defineComponent({
setup() {
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
const filesListWidth = useFileListWidth()
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
return {
currentView,
enabledFileActions,
filesListWidth,
}
},

View file

@ -48,6 +48,7 @@ import { defineComponent, inject } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { useNavigation } from '../../composables/useNavigation'
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
import { useRenamingStore } from '../../store/renaming.ts'
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
@ -75,10 +76,6 @@ export default defineComponent({
type: String,
required: true,
},
filesListWidth: {
type: Number,
required: true,
},
nodes: {
type: Array as PropType<Node[]>,
required: true,
@ -97,6 +94,7 @@ export default defineComponent({
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
const { directory } = useRouteParameters()
const filesListWidth = useFileListWidth()
const renamingStore = useRenamingStore()
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
@ -105,6 +103,7 @@ export default defineComponent({
currentView,
defaultFileAction,
directory,
filesListWidth,
renamingStore,
}

View file

@ -38,7 +38,6 @@
<FileEntryName ref="name"
:basename="basename"
:extension="extension"
:files-list-width="filesListWidth"
:grid-mode="true"
:nodes="nodes"
:source="source"
@ -58,7 +57,6 @@
<!-- Actions -->
<FileEntryActions ref="actions"
:class="`files-list__row-actions-${uniqueId}`"
:files-list-width="filesListWidth"
:grid-mode="true"
:loading.sync="loading"
:opened.sync="openedMenu"

View file

@ -43,10 +43,10 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.ts'
// The registered actions list
@ -62,10 +62,6 @@ export default defineComponent({
NcLoadingIcon,
},
mixins: [
filesListWidthMixin,
],
props: {
currentView: {
type: Object as PropType<View>,
@ -81,10 +77,12 @@ export default defineComponent({
const actionsMenuStore = useActionsMenuStore()
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
const fileListWidth = useFileListWidth()
const { directory } = useRouteParameters()
return {
directory,
fileListWidth,
actionsMenuStore,
filesStore,
@ -126,13 +124,13 @@ export default defineComponent({
},
inlineActions() {
if (this.filesListWidth < 512) {
if (this.fileListWidth < 512) {
return 0
}
if (this.filesListWidth < 768) {
if (this.fileListWidth < 768) {
return 1
}
if (this.filesListWidth < 1024) {
if (this.fileListWidth < 1024) {
return 2
}
return 3

View file

@ -12,7 +12,7 @@
isMtimeAvailable,
isSizeAvailable,
nodes,
filesListWidth,
fileListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
@ -39,7 +39,7 @@
<template #header>
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
:files-list-width="filesListWidth"
:files-list-width="fileListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
@ -48,7 +48,7 @@
<!-- Tfoot-->
<template #footer>
<FilesListTableFooter :current-view="currentView"
:files-list-width="filesListWidth"
:files-list-width="fileListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
@ -69,6 +69,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { defineComponent } from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { getSummaryFor } from '../utils/fileUtils'
import { useSelectionStore } from '../store/selection.js'
@ -79,7 +80,6 @@ import FileEntryGrid from './FileEntryGrid.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import VirtualList from './VirtualList.vue'
import logger from '../logger.ts'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
@ -97,10 +97,6 @@ export default defineComponent({
FilesListTableHeaderActions,
},
mixins: [
filesListWidthMixin,
],
props: {
currentView: {
type: View,
@ -119,10 +115,12 @@ export default defineComponent({
setup() {
const userConfigStore = useUserConfigStore()
const selectionStore = useSelectionStore()
const fileListWidth = useFileListWidth()
const { fileId, openFile } = useRouteParameters()
return {
fileId,
fileListWidth,
openFile,
userConfigStore,
@ -151,14 +149,14 @@ export default defineComponent({
isMtimeAvailable() {
// Hide mtime column on narrow screens
if (this.filesListWidth < 768) {
if (this.fileListWidth < 768) {
return false
}
return this.nodes.some(node => node.mtime !== undefined)
},
isSizeAvailable() {
// Hide size column on narrow screens
if (this.filesListWidth < 768) {
if (this.fileListWidth < 768) {
return false
}
return this.nodes.some(node => node.size !== undefined)

View file

@ -55,10 +55,9 @@
import type { File, Folder, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { defineComponent } from 'vue'
import debounce from 'debounce'
import Vue from 'vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.ts'
interface RecycledPoolItem {
@ -70,11 +69,9 @@ type DataSource = File | Folder
type DataSourceKey = keyof DataSource
export default Vue.extend({
export default defineComponent({
name: 'VirtualList',
mixins: [filesListWidthMixin],
props: {
dataComponent: {
type: [Object, Function],
@ -101,7 +98,7 @@ export default Vue.extend({
default: false,
},
/**
* Visually hidden caption for the table accesibility
* Visually hidden caption for the table accessibility
*/
caption: {
type: String,
@ -109,6 +106,14 @@ export default Vue.extend({
},
},
setup() {
const fileListWidth = useFileListWidth()
return {
fileListWidth,
}
},
data() {
return {
index: this.scrollToIndex,
@ -151,7 +156,7 @@ export default Vue.extend({
if (!this.gridMode) {
return 1
}
return Math.floor(this.filesListWidth / this.itemWidth)
return Math.floor(this.fileListWidth / this.itemWidth)
},
/**

View file

@ -0,0 +1,56 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { defineComponent } from 'vue'
import { useFileListWidth } from './useFileListWidth.ts'
const ComponentMock = defineComponent({
template: '<div id="test-component" style="width: 100%;background: white;">{{ fileListWidth }}</div>',
setup() {
return {
fileListWidth: useFileListWidth(),
}
},
})
const FileListMock = defineComponent({
template: '<main id="app-content-vue" style="width: 100%;"><component-mock /></main>',
components: {
ComponentMock,
},
})
describe('composable: fileListWidth', () => {
it('Has initial value', () => {
cy.viewport(600, 400)
cy.mount(FileListMock, {})
cy.get('#app-content-vue')
.should('be.visible')
.and('contain.text', '600')
})
it('Is reactive to size change', () => {
cy.viewport(600, 400)
cy.mount(FileListMock)
cy.get('#app-content-vue').should('contain.text', '600')
cy.viewport(800, 400)
cy.screenshot()
cy.get('#app-content-vue').should('contain.text', '800')
})
it('Is reactive to style changes', () => {
cy.viewport(600, 400)
cy.mount(FileListMock)
cy.get('#app-content-vue')
.should('be.visible')
.and('contain.text', '600')
.invoke('attr', 'style', 'width: 100px')
cy.get('#app-content-vue')
.should('contain.text', '100')
})
})

View file

@ -0,0 +1,50 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Ref } from 'vue'
import { onMounted, readonly, ref } from 'vue'
/** The element we observe */
let element: HTMLElement | undefined
/** The current width of the element */
const width = ref(0)
const observer = new ResizeObserver((elements) => {
if (elements[0].contentBoxSize) {
// use the newer `contentBoxSize` property if available
width.value = elements[0].contentBoxSize[0].inlineSize
} else {
// fall back to `contentRect`
width.value = elements[0].contentRect.width
}
})
/**
* Update the observed element if needed and reconfigure the observer
*/
function updateObserver() {
const el = document.querySelector<HTMLElement>('#app-content-vue') ?? document.body
if (el !== element) {
// if already observing: stop observing the old element
if (element) {
observer.unobserve(element)
}
// observe the new element if needed
observer.observe(el)
element = el
}
}
/**
* Get the reactive width of the file list
*/
export function useFileListWidth(): Readonly<Ref<number>> {
// Update the observer when the component is mounted (e.g. because this is the files app)
onMounted(updateObserver)
// Update the observer also in setup context, so we already have an initial value
updateObserver()
return readonly(width)
}

View file

@ -1,33 +0,0 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
filesListWidth: 0,
}
},
mounted() {
const fileListEl = document.querySelector('#app-content-vue')
this.filesListWidth = fileListEl?.clientWidth ?? 0
// @ts-expect-error The resize observer is just now attached to the object
this.$resizeObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0].target === fileListEl) {
this.filesListWidth = entries[0].contentRect.width
}
})
// @ts-expect-error The resize observer was attached right before to the this object
this.$resizeObserver.observe(fileListEl as Element)
},
beforeDestroy() {
// @ts-expect-error mounted must have been called before the destroy, so the resize
this.$resizeObserver.disconnect()
},
})

View file

@ -9,7 +9,7 @@
<BreadCrumbs :path="directory" @reload="fetchContent">
<template #actions>
<!-- Sharing button -->
<NcButton v-if="canShare && filesListWidth >= 512"
<NcButton v-if="canShare && fileListWidth >= 512"
:aria-label="shareButtonLabel"
:class="{ 'files-list__header-share-button--shared': shareButtonType }"
:title="shareButtonLabel"
@ -63,7 +63,7 @@
<!-- Secondary loading indicator -->
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
<NcButton v-if="filesListWidth >= 512 && enableGridView"
<NcButton v-if="fileListWidth >= 512 && enableGridView"
:aria-label="gridViewButtonLabel"
:title="gridViewButtonLabel"
class="files-list__header-grid-button"
@ -176,6 +176,7 @@ import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFilesStore } from '../store/files.ts'
import { useFiltersStore } from '../store/filters.ts'
@ -186,7 +187,6 @@ import { useUserConfigStore } from '../store/userconfig.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
import BreadCrumbs from '../components/BreadCrumbs.vue'
import FilesListVirtual from '../components/FilesListVirtual.vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.ts'
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
@ -219,7 +219,6 @@ export default defineComponent({
},
mixins: [
filesListWidthMixin,
filesSortingMixin,
],
@ -239,6 +238,7 @@ export default defineComponent({
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
const { currentView } = useNavigation()
const fileListWidth = useFileListWidth()
const { directory, fileId } = useRouteParameters()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
@ -248,6 +248,7 @@ export default defineComponent({
currentView,
directory,
fileId,
fileListWidth,
t,
filesStore,

4
dist/files-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