-
Notifications
You must be signed in to change notification settings - Fork 341
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
13 changed files
with
271 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<template> | ||
<div class="h-[110vh] w-full overflow-auto"> | ||
<VaStickyScrollbar /> | ||
|
||
<div class="h-full w-screen bg-gradient-to-l from-slate-300 to-slate-700" /> | ||
</div> | ||
</template> |
8 changes: 8 additions & 0 deletions
8
packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,5 +90,6 @@ export default [ | |
'VaMenuList', | ||
'VaMenuItem', | ||
'VaMenuGroup', | ||
'VaFormField' | ||
'VaFormField', | ||
'VaStickyScrollbar' | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ` | ||
<div style="width: 200px; height: 200vh; overflow: auto; position: relative;"> | ||
<div style="width: 300px; height: 300vh; background: linear-gradient(45deg, #fff, #000);" /> | ||
<VaStickyScrollbar direction="horizontal" /> | ||
</div> | ||
`, | ||
}), | ||
}) | ||
|
||
export const Vertical = defineStory({ | ||
story: () => ({ | ||
components: { VaStickyScrollbar }, | ||
|
||
template: ` | ||
<div style="width: 200vh; height: 200px; overflow: auto; position: relative;"> | ||
<div style="width: 300vh; height: 300px; background: linear-gradient(45deg, #fff, #000);" /> | ||
<VaStickyScrollbar direction="vertical" /> | ||
</div> | ||
`, | ||
}), | ||
}) | ||
|
||
/** Scrollbar reacts to content changes */ | ||
export const DynamicContent = defineStory({ | ||
story: () => ({ | ||
data () { | ||
return { | ||
showBlock2: false, | ||
} | ||
}, | ||
|
||
mounted () { | ||
setInterval(() => { | ||
this.showBlock2 = !this.showBlock2 | ||
}, 2000) | ||
}, | ||
|
||
components: { VaStickyScrollbar }, | ||
|
||
template: ` | ||
<div style="width: 200vh; height: 200px; overflow: auto; position: relative;"> | ||
<div v-if="showBlock2" style="width: 500vh; height: 300px; background: linear-gradient(45deg, #f0f, #0f0);" /> | ||
<div v-else style="width: 300vh; height: 300px; background: linear-gradient(45deg, #fff, #000);" /> | ||
<VaStickyScrollbar direction="vertical" /> | ||
</div> | ||
`, | ||
}), | ||
}) |
122 changes: 122 additions & 0 deletions
122
packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<script setup lang="ts"> | ||
import { computed, ref } from 'vue' | ||
import { getScrollbarSize } from '../../utils/scrollbar-size' | ||
import { useEvent, useElementRect, useResizeObserver } from '../../composables' | ||
const props = withDefaults(defineProps<{ | ||
el?: HTMLElement | null | ||
direction: 'vertical' | 'horizontal' | ||
}>(), { | ||
direction: 'horizontal', | ||
}) | ||
const currentEl = ref(null as HTMLElement | null) | ||
const parentElement = computed(() => { | ||
if (props.el) { return props.el } | ||
return currentEl.value?.parentNode as HTMLElement ?? null | ||
}) | ||
const parentRect = useElementRect(parentElement) | ||
const stickyScrollWrapperStyle = computed(() => { | ||
const el = parentElement.value | ||
if (!el) { return {} } | ||
const parentEl = el as HTMLElement | ||
const scrollSize = getScrollbarSize(parentEl) | ||
const { bottom, left, right, top } = parentRect.value | ||
if (props.direction === 'vertical') { | ||
if (left > window.innerWidth) { return { display: 'none' } } | ||
if (right < window.innerWidth) { return { display: 'none' } } | ||
return { | ||
position: 'fixed' as const, | ||
top: `${top}px`, | ||
right: 0, | ||
height: `${parentEl.clientHeight}px`, | ||
overflowY: 'auto' as const, | ||
overflowX: 'hidden' as const, | ||
} | ||
} | ||
if (top > window.innerHeight) { return { display: 'none' } } | ||
if (bottom < window.innerHeight) { return { display: 'none' } } | ||
return { | ||
position: 'fixed' as const, | ||
top: `${Math.min(bottom, window.innerHeight) - scrollSize}px`, | ||
width: `${parentEl.clientWidth}px`, | ||
overflowX: 'auto' as const, | ||
overflowY: 'hidden' as const, | ||
} | ||
}) | ||
useEvent('scroll', (e) => { | ||
if (!currentEl.value) { return } | ||
if (props.direction === 'horizontal') { | ||
parentElement.value?.scrollTo({ | ||
left: currentEl.value.scrollLeft, | ||
}) | ||
} else { | ||
parentElement.value?.scrollTo({ | ||
top: currentEl.value.scrollTop, | ||
}) | ||
} | ||
}, currentEl) | ||
useEvent('scroll', (e) => { | ||
if (!currentEl.value) { return } | ||
if (props.direction === 'horizontal') { | ||
if (parentElement.value?.scrollLeft === currentEl.value.scrollLeft) { return } | ||
currentEl.value.scrollTo({ | ||
left: parentElement.value?.scrollLeft, | ||
}) | ||
} else { | ||
if (parentElement.value?.scrollTop === currentEl.value.scrollTop) { return } | ||
currentEl.value.scrollTo({ | ||
top: parentElement.value?.scrollTop, | ||
}) | ||
} | ||
}, parentElement) | ||
const scrollWidth = ref(0) | ||
const scrollHeight = ref(0) | ||
useResizeObserver(computed(() => { | ||
if (!parentElement.value) { return [] } | ||
return [...parentElement.value.children] as HTMLElement[] | ||
}), () => { | ||
scrollWidth.value = parentElement.value.scrollWidth | ||
scrollHeight.value = parentElement.value.scrollHeight | ||
}) | ||
const fakeContentStyle = computed(() => { | ||
if (props.direction === 'vertical') { | ||
return { | ||
width: '1px', | ||
height: `${scrollHeight.value}px`, | ||
} | ||
} | ||
return { | ||
height: '1px', | ||
width: `${scrollWidth.value}px`, | ||
} | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div :style="stickyScrollWrapperStyle" ref="currentEl"> | ||
<div :style="fakeContentStyle"></div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as VaStickyScrollbar } from './StickyScrollbar.vue' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { Ref, onBeforeUnmount, onMounted, ref } from 'vue' | ||
|
||
export const useElementRect = (element: Ref<HTMLElement | null>) => { | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,4 +92,5 @@ export { | |
VaMenuItem, | ||
VaMenuGroup, | ||
VaFormField, | ||
VaStickyScrollbar, | ||
} from '../../components' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |