diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue
index c423b698d40d8..3569228dbde0f 100644
--- a/apps/files/src/components/BreadCrumbs.vue
+++ b/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
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index fc5c7fb97f625..7af76c87c4318 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -36,7 +36,6 @@
@@ -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,
}
},
diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue
index 8c150b780876b..f8fde7842a879 100644
--- a/apps/files/src/components/FileEntry/FileEntryActions.vue
+++ b/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('enabledFileActions', [])
return {
currentView,
enabledFileActions,
+ filesListWidth,
}
},
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
index e4cffba32b79d..1eff841738b54 100644
--- a/apps/files/src/components/FileEntry/FileEntryName.vue
+++ b/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,
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('defaultFileAction')
@@ -105,6 +103,7 @@ export default defineComponent({
currentView,
defaultFileAction,
directory,
+ filesListWidth,
renamingStore,
}
diff --git a/apps/files/src/components/FileEntryGrid.vue b/apps/files/src/components/FileEntryGrid.vue
index f0b086ac891d8..0b0344afb99ae 100644
--- a/apps/files/src/components/FileEntryGrid.vue
+++ b/apps/files/src/components/FileEntryGrid.vue
@@ -38,7 +38,6 @@
,
@@ -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
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index d4c3d9495b717..52ba69d8b9794 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/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 @@
@@ -48,7 +48,7 @@
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)
diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue
index 4c047a76a4e47..d2b436344a5cf 100644
--- a/apps/files/src/components/VirtualList.vue
+++ b/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)
},
/**
diff --git a/apps/files/src/composables/useFileListWidth.cy.ts b/apps/files/src/composables/useFileListWidth.cy.ts
new file mode 100644
index 0000000000000..b0d42c4a2d6c6
--- /dev/null
+++ b/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: '{{ fileListWidth }}
',
+ setup() {
+ return {
+ fileListWidth: useFileListWidth(),
+ }
+ },
+})
+const FileListMock = defineComponent({
+ template: '',
+ 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')
+ })
+})
diff --git a/apps/files/src/composables/useFileListWidth.ts b/apps/files/src/composables/useFileListWidth.ts
new file mode 100644
index 0000000000000..621ef20483642
--- /dev/null
+++ b/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('#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[> {
+ // 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)
+}
diff --git a/apps/files/src/mixins/filesListWidth.ts b/apps/files/src/mixins/filesListWidth.ts
deleted file mode 100644
index 7d7ec59867357..0000000000000
--- a/apps/files/src/mixins/filesListWidth.ts
+++ /dev/null
@@ -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()
- },
-})
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index 56907db3febd1..6cbaecfa02302 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -9,7 +9,7 @@
-
- ]