Browse Source

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

refactor(files): Provide `useFileListWidth` composable
Ferdinand Thiessen 1 day ago
parent
commit
5a8d32fe53

+ 7 - 9
apps/files/src/components/BreadCrumbs.vue

@@ -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

+ 3 - 2
apps/files/src/components/FileEntry.vue

@@ -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,
 		}
 	},
 

+ 4 - 4
apps/files/src/components/FileEntry/FileEntryActions.vue

@@ -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,
 		}
 	},
 

+ 3 - 4
apps/files/src/components/FileEntry/FileEntryName.vue

@@ -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,
 		}

+ 0 - 2
apps/files/src/components/FileEntryGrid.vue

@@ -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"

+ 6 - 8
apps/files/src/components/FilesListTableHeaderActions.vue

@@ -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

+ 8 - 10
apps/files/src/components/FilesListVirtual.vue

@@ -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)

+ 13 - 8
apps/files/src/components/VirtualList.vue

@@ -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)
 		},
 
 		/**

+ 56 - 0
apps/files/src/composables/useFileListWidth.cy.ts

@@ -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')
+	})
+})

+ 50 - 0
apps/files/src/composables/useFileListWidth.ts

@@ -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)
+}

+ 0 - 33
apps/files/src/mixins/filesListWidth.ts

@@ -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()
-	},
-})

+ 5 - 4
apps/files/src/views/FilesList.vue

@@ -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,

File diff suppressed because it is too large
+ 0 - 0
dist/files-main.js


File diff suppressed because it is too large
+ 0 - 0
dist/files-main.js.map


Some files were not shown because too many files changed in this diff