From 2b3907a0ecabb6141a74e96849238572c38e7d65 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Thu, 27 Jun 2024 10:59:13 +0300 Subject: [PATCH] feat(sticky-scrollbar): init component (#4286) * feat(sticky-scrollbar): init component * fix: expect scroll content to change * fix: remove extra console logs * fix: improve perf --- packages/docs/page-config/navigationRoutes.ts | 7 + .../sticky-scrollbar/examples/Horizontal.vue | 7 + .../ui-elements/sticky-scrollbar/index.ts | 8 ++ packages/nuxt/src/config/components.ts | 3 +- packages/ui/src/components/index.ts | 1 + .../StickyScrollbar.stories.ts | 61 +++++++++ .../va-sticky-scrollbar/StickyScrollbar.vue | 122 ++++++++++++++++++ .../components/va-sticky-scrollbar/index.ts | 1 + packages/ui/src/composables/index.ts | 1 + packages/ui/src/composables/useElementRect.ts | 41 ++++++ .../ui/src/composables/useResizeObserver.ts | 15 ++- .../ui/src/services/vue-plugin/components.ts | 1 + packages/ui/src/utils/scrollbar-size.ts | 7 + 13 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue create mode 100644 packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts create mode 100644 packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts create mode 100644 packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue create mode 100644 packages/ui/src/components/va-sticky-scrollbar/index.ts create mode 100644 packages/ui/src/composables/useElementRect.ts create mode 100644 packages/ui/src/utils/scrollbar-size.ts diff --git a/packages/docs/page-config/navigationRoutes.ts b/packages/docs/page-config/navigationRoutes.ts index 9f3d3e43f4..4a5a21231c 100644 --- a/packages/docs/page-config/navigationRoutes.ts +++ b/packages/docs/page-config/navigationRoutes.ts @@ -427,6 +427,13 @@ export const navigationRoutes: NavigationRoute[] = [ badge : navigationBadge.new('1.6.0 '), } }, + { + name: 'sticky-scrollbar', + displayName: 'Sticky Scrollbar', + meta: { + badge : navigationBadge.new('1.9.10'), + } + }, { name: "hover", displayName: "Hover", diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue new file mode 100644 index 0000000000..9aad4cf63b --- /dev/null +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue @@ -0,0 +1,7 @@ + diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts new file mode 100644 index 0000000000..b891fa6374 --- /dev/null +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts @@ -0,0 +1,8 @@ +export default definePageConfig({ + blocks: [ + block.title("StickyScrollbar"), + block.paragraph("This component adds floating scrollbar to element if it doesn't fit the screen. In case if you have large scroll container and want your scrollbar to be always visible, you need to use this component."), + + block.example('Horizontal') + ], +}); diff --git a/packages/nuxt/src/config/components.ts b/packages/nuxt/src/config/components.ts index 27ec9f4808..fd47a46669 100644 --- a/packages/nuxt/src/config/components.ts +++ b/packages/nuxt/src/config/components.ts @@ -90,5 +90,6 @@ export default [ 'VaMenuList', 'VaMenuItem', 'VaMenuGroup', - 'VaFormField' + 'VaFormField', + 'VaStickyScrollbar' ] diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 819f5ce235..263029f823 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -92,3 +92,4 @@ export * from './va-file-upload' export * from './va-textarea' export * from './va-menu' export * from './va-form-field' +export * from './va-sticky-scrollbar' diff --git a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts new file mode 100644 index 0000000000..357b656c50 --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts @@ -0,0 +1,61 @@ +import { defineComponent } from 'vue' +import { defineStory } from '../../../.storybook/types' +import VaStickyScrollbar from './StickyScrollbar.vue' + +export default { + title: 'VaStickyScrollbar', + component: VaStickyScrollbar, +} + +export const Horizontal = defineStory({ + story: () => ({ + components: { VaStickyScrollbar }, + + template: ` +
+
+ +
+ `, + }), +}) + +export const Vertical = defineStory({ + story: () => ({ + components: { VaStickyScrollbar }, + + template: ` +
+
+ +
+ `, + }), +}) + +/** Scrollbar reacts to content changes */ +export const DynamicContent = defineStory({ + story: () => ({ + data () { + return { + showBlock2: false, + } + }, + + mounted () { + setInterval(() => { + this.showBlock2 = !this.showBlock2 + }, 2000) + }, + + components: { VaStickyScrollbar }, + + template: ` +
+
+
+ +
+ `, + }), +}) diff --git a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue new file mode 100644 index 0000000000..1165ccf38c --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue @@ -0,0 +1,122 @@ + + + diff --git a/packages/ui/src/components/va-sticky-scrollbar/index.ts b/packages/ui/src/components/va-sticky-scrollbar/index.ts new file mode 100644 index 0000000000..2e5eeb27ea --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/index.ts @@ -0,0 +1 @@ +export { default as VaStickyScrollbar } from './StickyScrollbar.vue' diff --git a/packages/ui/src/composables/index.ts b/packages/ui/src/composables/index.ts index 238410fd9e..9f92c7a5d8 100644 --- a/packages/ui/src/composables/index.ts +++ b/packages/ui/src/composables/index.ts @@ -75,3 +75,4 @@ export * from './useElementTextColor' export * from './useElementBackground' export * from './useImmediateFocus' export * from './useNumericProp' +export * from './useElementRect' diff --git a/packages/ui/src/composables/useElementRect.ts b/packages/ui/src/composables/useElementRect.ts new file mode 100644 index 0000000000..24b3db0513 --- /dev/null +++ b/packages/ui/src/composables/useElementRect.ts @@ -0,0 +1,41 @@ +import { Ref, onBeforeUnmount, onMounted, ref } from 'vue' + +export const useElementRect = (element: Ref) => { + const rect = ref({ top: 0, left: 0, width: 0, height: 0, bottom: 0, right: 0 }) satisfies Ref<{ + top: number + left: number + width: number + height: number + bottom: number + right: number + }> + + let resizeObserver: ResizeObserver | undefined + let mutationObserver: MutationObserver | undefined + + const updateRect = () => { + if (element.value) { + rect.value = element.value.getBoundingClientRect() + } + } + + onMounted(() => { + resizeObserver = new ResizeObserver(updateRect) + mutationObserver = new MutationObserver(updateRect) + + element.value && resizeObserver.observe(element.value) + element.value && mutationObserver.observe(element.value, { attributes: true, childList: true, subtree: true }) + + updateRect() + }) + + onBeforeUnmount(() => { + resizeObserver?.disconnect() + mutationObserver?.disconnect() + + resizeObserver = undefined + mutationObserver = undefined + }) + + return rect +} diff --git a/packages/ui/src/composables/useResizeObserver.ts b/packages/ui/src/composables/useResizeObserver.ts index 248d2bb8ee..c28b979e32 100644 --- a/packages/ui/src/composables/useResizeObserver.ts +++ b/packages/ui/src/composables/useResizeObserver.ts @@ -3,7 +3,15 @@ import { onBeforeUnmount, onMounted, ref, Ref, unref, watch } from 'vue' type MaybeRef = T | Ref type MaybeArray = T | T[] -export const useResizeObserver = (elementsList: MaybeRef[] | Ref, cb: ResizeObserverCallback) => { +const normalizeElements = (elements: MaybeRef[] | Ref>) => { + return Array.isArray(elements) + ? elements + : Array.isArray(elements.value) + ? elements.value + : [elements.value] +} + +export const useResizeObserver = (elementsList: MaybeRef[] | Ref>, cb: ResizeObserverCallback) => { let resizeObserver: ResizeObserver | undefined const observeAll = (elementsList: MaybeRef[]) => { @@ -16,12 +24,13 @@ export const useResizeObserver = (elementsLis watch(elementsList, (newValue) => { resizeObserver?.disconnect() - observeAll(Array.isArray(newValue) ? newValue : [newValue]) + + observeAll(normalizeElements(newValue)) }) onMounted(() => { resizeObserver = new ResizeObserver(cb) - observeAll(Array.isArray(elementsList) ? elementsList : [elementsList]) + observeAll(normalizeElements(elementsList)) }) onBeforeUnmount(() => resizeObserver?.disconnect()) diff --git a/packages/ui/src/services/vue-plugin/components.ts b/packages/ui/src/services/vue-plugin/components.ts index a1fd0f4e7f..28206df391 100644 --- a/packages/ui/src/services/vue-plugin/components.ts +++ b/packages/ui/src/services/vue-plugin/components.ts @@ -92,4 +92,5 @@ export { VaMenuItem, VaMenuGroup, VaFormField, + VaStickyScrollbar, } from '../../components' diff --git a/packages/ui/src/utils/scrollbar-size.ts b/packages/ui/src/utils/scrollbar-size.ts new file mode 100644 index 0000000000..77b03fe556 --- /dev/null +++ b/packages/ui/src/utils/scrollbar-size.ts @@ -0,0 +1,7 @@ +export const getScrollbarSize = (element: HTMLElement | null | undefined) => { + if (!element) { return 0 } + + const scrollbarWidth = element.offsetWidth - element.clientWidth + const scrollbarHeight = element.offsetHeight - element.clientHeight + return Math.max(scrollbarWidth, scrollbarHeight) +}