diff --git a/changelog/unreleased/change-remove-implicit-registration b/changelog/unreleased/change-remove-implicit-registration
new file mode 100644
index 000000000..c795b66b6
--- /dev/null
+++ b/changelog/unreleased/change-remove-implicit-registration
@@ -0,0 +1,5 @@
+Change: Remove implicit ODS registration
+
+Remove implicit registration of ODS, from now on applications using ODS must register it explicit via `Vue.use`.
+
+https://github.com/owncloud/owncloud-design-system/pull/1848
diff --git a/changelog/unreleased/enhancement-add-composition-api b/changelog/unreleased/enhancement-add-composition-api
new file mode 100644
index 000000000..9bdc0c2f0
--- /dev/null
+++ b/changelog/unreleased/enhancement-add-composition-api
@@ -0,0 +1,6 @@
+Enhancement: make Vue-Composition-API available
+
+To support upcoming Vue composition-api we`ve added the compatibility layer from the creators.
+From now on all features described here `https://github.com/vuejs/composition-api` can be used.
+
+https://github.com/owncloud/owncloud-design-system/pull/1848
diff --git a/changelog/unreleased/enhancement-lazy-table-cells b/changelog/unreleased/enhancement-lazy-table-cells
new file mode 100644
index 000000000..0d0b61363
--- /dev/null
+++ b/changelog/unreleased/enhancement-lazy-table-cells
@@ -0,0 +1,14 @@
+Enhancement: Add option to render table cells lazy
+
+In cases where the table (`OcTable only`) has multiple child rows with many cells, it can be a bottleneck to rendered all of them immediately.
+With this in mind we've added the lazy option to the table fields object where the consuming app can decide how lazy rendering should behave.
+
+By default lazy cell rendering is disabled, to enable it add a lazy object to the field.
+
+following options are available:
+* `delay: 250` - when the cell visibility on screen is below given ms value rendering gets skipped.
+* `mode: show` - cell gets rendered and stays painted, no de-rendering happens.
+* `mode: showHide` - cell gets rendered when it enters the screen and de-rendered when its off.
+* `rootMargin: 100px` - given value will be added to the outer area of the element which then increases the visibility detection radius
+
+https://github.com/owncloud/owncloud-design-system/pull/1848
diff --git a/docs/docs.helper.js b/docs/docs.helper.js
index e3b3dbf0e..2c52beab8 100644
--- a/docs/docs.helper.js
+++ b/docs/docs.helper.js
@@ -3,6 +3,7 @@
* You can add more things if/when needed.
*/
import Vue from "vue"
+import VueCompositionAPI from "@vue/composition-api"
import statusLabels from "./utils/statusLabels"
import activeNav from "./utils/activeNav"
import filterSearch from "./utils/filterSearch"
@@ -13,6 +14,7 @@ import GetTextPlugin from "vue-gettext"
Vue.config.productionTip = false
Vue.mixin(statusLabels)
Vue.use(GetTextPlugin, { translations: {} })
+Vue.use(VueCompositionAPI)
document.addEventListener("DOMContentLoaded", () => {
filterSearch.methods.init()
diff --git a/package.json b/package.json
index 8d8b05bdb..f7fbff90e 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"@babel/plugin-transform-runtime": "^7.16.5",
"@babel/preset-env": "^7.14.4",
"@popperjs/core": "^2.4.0",
+ "@vue/composition-api": "^1.4.3",
"@vue/test-utils": "^1.2.0",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
@@ -135,6 +136,7 @@
},
"peerDependencies": {
"@popperjs/core": "^2.4.0",
+ "@vue/composition-api": "^1.4.3",
"filesize": "^8.0.0",
"focus-trap": "^6.4.0",
"focus-trap-vue": "^1.1.1",
diff --git a/src/components/atoms/_OcTableCellData/_OcTableCellData.vue b/src/components/atoms/_OcTableCellData/_OcTableCellData.vue
index a65846fe5..9b4d14030 100644
--- a/src/components/atoms/_OcTableCellData/_OcTableCellData.vue
+++ b/src/components/atoms/_OcTableCellData/_OcTableCellData.vue
@@ -1,5 +1,6 @@
-
+
+
+
diff --git a/src/components/molecules/OcTable/OcTable.spec.js b/src/components/molecules/OcTable/OcTable.spec.js
index 9565371ad..f02e8fa5a 100644
--- a/src/components/molecules/OcTable/OcTable.spec.js
+++ b/src/components/molecules/OcTable/OcTable.spec.js
@@ -1,8 +1,11 @@
-import { shallowMount, mount } from "@vue/test-utils"
-const { axe, toHaveNoViolations } = require("jest-axe")
-
+import { shallowMount, mount, createLocalVue } from "@vue/test-utils"
+import VueCompositionAPI from "@vue/composition-api"
+import { axe, toHaveNoViolations } from "jest-axe"
import Table from "./OcTable.vue"
+const localVue = createLocalVue()
+localVue.use(VueCompositionAPI)
+
expect.extend(toHaveNoViolations)
const fields = [
@@ -50,6 +53,7 @@ const data = [
describe("OcTable", () => {
it("displays all field types", async () => {
const wrapper = mount(Table, {
+ localVue,
propsData: {
fields,
data,
diff --git a/src/components/molecules/OcTable/OcTable.vue b/src/components/molecules/OcTable/OcTable.vue
index 957f1ead8..a8a489a0d 100644
--- a/src/components/molecules/OcTable/OcTable.vue
+++ b/src/components/molecules/OcTable/OcTable.vue
@@ -388,6 +388,10 @@ export default {
props["aria-label"] = field.accessibleLabelCallback(item)
}
+ if (Object.prototype.hasOwnProperty.call(field, "lazy")) {
+ props.lazy = field.lazy
+ }
+
return props
},
extractCellProps(field) {
@@ -538,7 +542,10 @@ export default {
return [{
name: "resource",
title: "Resource",
- alignH: "left"
+ alignH: "left",
+ lazy: {
+ delay: 1500
+ }
}, {
name: "last_modified",
title: "Last modified",
diff --git a/src/composables/index.js b/src/composables/index.js
new file mode 100644
index 000000000..4ce413390
--- /dev/null
+++ b/src/composables/index.js
@@ -0,0 +1 @@
+export * from "./useIsVisible"
diff --git a/src/composables/useIsVisible/index.js b/src/composables/useIsVisible/index.js
new file mode 100644
index 000000000..a948bf6df
--- /dev/null
+++ b/src/composables/useIsVisible/index.js
@@ -0,0 +1,68 @@
+import { onBeforeUnmount, ref, watch } from "@vue/composition-api"
+
+/**
+ * once ODS has lodash this debounce implementation can be replaced with the one from lodash.
+ * @param delay
+ * @param callback
+ * @returns {(function(...[*]=): void)|*}
+ */
+const debounce = (delay = 0, callback) => {
+ let id = null
+ return (...args) => {
+ window.clearTimeout(id)
+ id = window.setTimeout(() => {
+ callback.apply(null, args)
+ }, delay)
+ }
+}
+
+/**
+ *
+ * @param {Ref} target - ref with element to be observed
+ * @param {('show'|'showHide')} mode - showHide shows and hides the element on screen enter or leave, show only detects entering the screen and the keeps it rendered
+ * @param {string} rootMargin - margin that will be added around the element to detect visibility
+ * @param {number} delay - defines the debounce delay of the visibility detection
+ * @returns {{isVisible: Ref}}
+ */
+export const useIsVisible = ({ target, mode = "show", rootMargin = "100px", delay = 0 }) => {
+ const isSupported = window && "IntersectionObserver" in window
+ if (!isSupported) {
+ return {
+ isVisible: ref(true),
+ }
+ }
+
+ const isVisible = ref(false)
+ const observer = new IntersectionObserver(
+ debounce(delay, ([{ isIntersecting }]) => {
+ isVisible.value = isIntersecting
+ /**
+ * if given mode is `showHide` we need to keep the observation alive.
+ */
+ if (mode === "showHide") {
+ return
+ }
+ /**
+ * if the mode is `show` which is the default, the implementation needs to unsubscribe the target from the observer
+ */
+ if (!isVisible.value) {
+ return
+ }
+
+ observer.unobserve(target.value)
+ }),
+ {
+ rootMargin,
+ }
+ )
+
+ watch(target, () => {
+ observer.observe(target.value)
+ })
+
+ onBeforeUnmount(() => observer.disconnect())
+
+ return {
+ isVisible,
+ }
+}
diff --git a/src/composables/useIsVisible/index.spec.js b/src/composables/useIsVisible/index.spec.js
new file mode 100644
index 000000000..b029a5509
--- /dev/null
+++ b/src/composables/useIsVisible/index.spec.js
@@ -0,0 +1,127 @@
+import { createLocalVue, mount } from "@vue/test-utils"
+import VueCompositionAPI, { ref, nextTick } from "@vue/composition-api"
+import { useIsVisible } from "./index"
+
+const localVue = createLocalVue()
+localVue.use(VueCompositionAPI)
+
+const mockIntersectionObserver = () => {
+ jest.useFakeTimers()
+
+ const enable = () => {
+ const mock = {
+ observe: jest.fn(),
+ disconnect: jest.fn(),
+ unobserve: jest.fn(),
+ }
+
+ window.IntersectionObserver = jest.fn().mockImplementation(() => mock)
+
+ return {
+ mock,
+ callback: (args, fastForward = 0) => {
+ window.IntersectionObserver.mock.calls[0][0](args)
+ jest.advanceTimersByTime(fastForward)
+ },
+ }
+ }
+
+ const disable = () => {
+ delete window.IntersectionObserver
+ }
+
+ return { enable, disable }
+}
+
+const createWrapper = (options = {}) =>
+ mount(
+ {
+ template: `
+ `,
+ setup: () => {
+ const target = ref()
+ const { isVisible } = useIsVisible({ ...options, target })
+
+ return {
+ isVisible,
+ target,
+ }
+ },
+ },
+ {
+ localVue,
+ }
+ )
+
+describe("useIsVisible", () => {
+ const { enable: enableIntersectionObserver, disable: disableIntersectionObserver } =
+ mockIntersectionObserver()
+
+ it("is visible by default if browser does not support IntersectionObserver", () => {
+ disableIntersectionObserver()
+ const wrapper = createWrapper()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe("true")
+ })
+
+ it("observes the target", async () => {
+ const { mock: observerMock } = enableIntersectionObserver()
+ createWrapper()
+ await nextTick()
+
+ expect(observerMock.observe).toBeCalledTimes(1)
+ })
+
+ it("only shows once and then gets unobserved if the the composable is in the default show mode", async () => {
+ const { mock: observerMock, callback: observerCallback } = enableIntersectionObserver()
+ const wrapper = createWrapper()
+
+ await nextTick()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe("false")
+
+ observerCallback([{ isIntersecting: true }])
+ await nextTick()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe("true")
+ expect(observerMock.unobserve).toBeCalledTimes(1)
+ })
+
+ it("shows and hides multiple times if the the composable is in showHide mode", async () => {
+ const { mock: observerMock, callback: observerCallback } = enableIntersectionObserver()
+ const wrapper = createWrapper({ mode: "showHide" })
+
+ await nextTick()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe("false")
+
+ observerCallback([{ isIntersecting: true }])
+ await nextTick()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe("true")
+ expect(observerMock.unobserve).toBeCalledTimes(0)
+ })
+
+ it("gets delayed by a given value if many calls happen fast", async () => {
+ const { callback: observerCallback } = enableIntersectionObserver()
+ const wrapper = createWrapper({ delay: 5000, mode: "showHide" })
+
+ const checkIsVisibleAfter = async (expects, fastForward, isIntersecting) => {
+ observerCallback([{ isIntersecting }], fastForward)
+ await nextTick()
+ expect(wrapper.vm.$refs.target.innerHTML).toBe(String(expects))
+ }
+
+ await checkIsVisibleAfter(false, 4000, true)
+ await checkIsVisibleAfter(false, 2000, false)
+ await checkIsVisibleAfter(true, 5000, true)
+ await checkIsVisibleAfter(true, 4800, false)
+ await checkIsVisibleAfter(false, 10000, false)
+ })
+
+ it("disconnects the observer before component gets unmounted", () => {
+ const { mock: observerMock } = enableIntersectionObserver()
+ const wrapper = createWrapper()
+
+ expect(observerMock.disconnect).toBeCalledTimes(0)
+ wrapper.vm.$destroy()
+ expect(observerMock.disconnect).toBeCalledTimes(1)
+ })
+})
diff --git a/src/system.js b/src/system.js
index 078cde53d..4ff47de65 100644
--- a/src/system.js
+++ b/src/system.js
@@ -40,10 +40,5 @@ const System = {
},
}
-// Automatic installation if Vue has been added to the global scope
-if (typeof window !== "undefined" && window.Vue) {
- window.Vue.use(System)
-}
-
// Finally export as default
export default System
diff --git a/yarn.lock b/yarn.lock
index db6848edb..33fcc0708 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1831,6 +1831,13 @@
optionalDependencies:
prettier "^1.18.2 || ^2.0.0"
+"@vue/composition-api@^1.4.3":
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-1.4.3.tgz#c80eb8c692e16ebfcdab4af5344a6e3ff8ccc38e"
+ integrity sha512-Qp4rMbESO05/7/Imck027X5lPhbmMX/mtYSDvIMJ14PS4KHY/4GllnQbPEfsBEe1LECFE6HWx2k7HYgcuYNvpg==
+ dependencies:
+ tslib "^2.3.1"
+
"@vue/ref-transform@3.2.21":
version "3.2.21"
resolved "https://registry.yarnpkg.com/@vue/ref-transform/-/ref-transform-3.2.21.tgz#b0c554c9f640c3f005f77e676066aa0faba90984"