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: ` +
+
{{ isVisible }}
+
`, + 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"