From c4fd60fdd5880e8af48328394d451caa603b43c7 Mon Sep 17 00:00:00 2001 From: Stanislav Lashmanov Date: Tue, 21 Oct 2025 21:14:19 +0400 Subject: [PATCH] Sticky sidebar for work items with proper height --- .../sticky_viewport_filler_height.vue | 44 +-- .../components/work_item_detail.vue | 286 +++++++++--------- .../framework/application-chrome.scss | 1 - .../stylesheets/page_bundles/work_items.scss | 12 +- 4 files changed, 180 insertions(+), 163 deletions(-) diff --git a/app/assets/javascripts/diffs/components/sticky_viewport_filler_height.vue b/app/assets/javascripts/diffs/components/sticky_viewport_filler_height.vue index bc1252c45ea001..a985ed974c95a6 100644 --- a/app/assets/javascripts/diffs/components/sticky_viewport_filler_height.vue +++ b/app/assets/javascripts/diffs/components/sticky_viewport_filler_height.vue @@ -37,31 +37,36 @@ export default { // 6ms is enough to target 120fps default: 6, }, + getOffsetParent: { + type: Function, + required: false, + default() { + return this.$refs.root.parentElement; + }, + }, }, data() { return { visible: false, currentTop: 0, - parentRect: { bottom: 0, height: 0 }, + offsetParent: null, + offsetParentRect: { bottom: 0, height: 0 }, + offsetParentObserver: null, viewportHeight: 0, rootObserver: null, - parentObserver: null, }; }, computed: { - parent() { - return this.$refs.root.parentElement; - }, throttledSampleRects() { return throttle(this.sampleRects, this.samplingRate, { leading: true }); }, endReached() { - return this.viewportHeight > this.parentRect.bottom; + return this.viewportHeight > this.offsetParentRect.bottom; }, availableHeight() { // parent is fully scrolled, the sticky element is pushed from both top and bottom if (this.endReached) { - return this.parentRect.bottom - Math.max(this.currentTop, this.stickyTopOffset); + return this.offsetParentRect.bottom - Math.max(this.currentTop, this.stickyTopOffset); } return this.viewportHeight - this.currentTop - this.stickyBottomOffset; }, @@ -74,21 +79,22 @@ export default { visible(isVisible) { if (isVisible) { this.sampleRects(); - this.observerParentResize(); + this.observerOffsetParentResize(); this.observeViewportChanges(); } else { - this.disconnectParent(); + this.disconnectOffsetParent(); this.disconnectViewport(); } }, }, mounted() { + this.offsetParent = this.getOffsetParent(); this.observeRootVisibility(); this.cacheViewportHeight(); }, beforeDestroy() { this.disconnectRoot(); - this.disconnectParent(); + this.disconnectOffsetParent(); this.disconnectViewport(); }, methods: { @@ -99,9 +105,9 @@ export default { observeElementOnce(this.$refs.root, ([root]) => { this.currentTop = root.boundingClientRect.top; }); - observeElementOnce(this.parent, ([parent]) => { + observeElementOnce(this.offsetParent, ([parent]) => { const { bottom, height } = parent.boundingClientRect; - this.parentRect = { bottom, height }; + this.offsetParentRect = { bottom, height }; }); }, observeRootVisibility() { @@ -110,10 +116,10 @@ export default { }); this.rootObserver.observe(this.$refs.root); }, - observerParentResize() { + observerOffsetParentResize() { // parent could grow, we might no longer be at the bottom of the parent element - this.parentObserver = new ResizeObserver(throttle(this.sampleRects, 20)); - this.parentObserver.observe(this.parent); + this.offsetParentObserver = new ResizeObserver(throttle(this.sampleRects, 20)); + this.offsetParentObserver.observe(this.offsetParent); }, observeViewportChanges() { window.addEventListener('scroll', this.throttledSampleRects, { passive: true }); @@ -124,10 +130,10 @@ export default { this.rootObserver.disconnect(); this.rootObserver = null; }, - disconnectParent() { - if (!this.parentObserver) return; - this.parentObserver.disconnect(); - this.parentObserver = null; + disconnectOffsetParent() { + if (!this.offsetParentObserver) return; + this.offsetParentObserver.disconnect(); + this.offsetParentObserver = null; }, disconnectViewport() { this.throttledSampleRects.cancel(); diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 1ddd8bb8347034..92d4847755e1fa 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -25,6 +25,7 @@ import { sanitize } from '~/lib/dompurify'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; import { keysFor, ISSUABLE_EDIT_DESCRIPTION } from '~/behaviors/shortcuts/keybindings'; import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items'; +import StickyViewportFillerHeight from '~/diffs/components/sticky_viewport_filler_height.vue'; import { i18n, WIDGET_TYPE_ASSIGNEES, @@ -110,6 +111,7 @@ export default { SHOW_SIDEBAR_STORAGE_KEY: 'work_item_show_sidebar', ENABLE_TRUNCATION_STORAGE_KEY: 'work_item_truncate_descriptions', components: { + StickyViewportFillerHeight, DesignDropzone, DesignWidget, DesignUploadButton, @@ -1145,6 +1147,7 @@ export default {
- - -
-

{{ s__('WorkItem|Attributes') }}

- -
- + - + - - - + + + - - + + - + - + - + + +
+ +

{{ s__('WorkItem|Attributes') }}

+ +
+
diff --git a/app/assets/stylesheets/framework/application-chrome.scss b/app/assets/stylesheets/framework/application-chrome.scss index 6114804f4daaef..deeb3d0ff70415 100644 --- a/app/assets/stylesheets/framework/application-chrome.scss +++ b/app/assets/stylesheets/framework/application-chrome.scss @@ -865,7 +865,6 @@ &:where(.application-chrome) { .work-item-attributes-wrapper { top: calc(#{$work-item-sticky-header-height} + #{$gl-spacing-scale-3}) !important; - height: calc(var(--panel-content-inner-height) - var(--work-item-sticky-header-height) - 1.5rem + 1px) !important; // 1.5rem is to account for padding, the +1px is to account for a border } } diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index 94ed5496a2cc9c..b78e5888308769 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -70,6 +70,13 @@ $work-item-overview-gap-width: 2rem; } } +.work-item-sticky-sidebar { + position: sticky; + top: calc(#{$calc-application-header-height} + #{$work-item-sticky-header-height}); + overflow-y: auto; + overflow-x: hidden; +} + .create-work-item-description { .markdown-area, .rte-text-box { @@ -81,13 +88,8 @@ $work-item-overview-gap-width: 2rem; @include gl-container-width-up(md) { .work-item-attributes-wrapper { - top: calc(#{$calc-application-header-height} + #{$work-item-sticky-header-height}); - height: calc(#{$calc-application-viewport-height} - #{$work-item-sticky-header-height}); margin-bottom: calc(#{$content-wrapper-padding} * -1); padding-inline: $gl-spacing-scale-3; - position: sticky; - overflow-y: auto; - overflow-x: hidden; } .work-item-overview-right-sidebar { -- GitLab