diff --git a/frontend/__tests__/components/GStatusTag.spec.js b/frontend/__tests__/components/GStatusTag.spec.js index b2440311eb..713850ebc9 100644 --- a/frontend/__tests__/components/GStatusTag.spec.js +++ b/frontend/__tests__/components/GStatusTag.spec.js @@ -56,7 +56,7 @@ describe('components', () => { expect(vm.chipTooltip.title).toBe('foo-bar') expect(vm.chipIcon).toBe('') expect(vm.isError || vm.isUnknown || vm.isProgressing).toBe(false) - expect(vm.color).toBe('primary') + expect(vm.color).toBe('success') expect(vm.visible).toBe(true) }) @@ -89,7 +89,7 @@ describe('components', () => { const vm = wrapper.vm expect(vm.visible).toBe(false) expect(vm.isProgressing).toBe(true) - expect(vm.color).toBe('primary') + expect(vm.color).toBe('success') expect(vm.chipStatus).toBe('Progressing') expect(vm.chipIcon).toBe('') }) diff --git a/frontend/__tests__/components/GStatusTags.spec.js b/frontend/__tests__/components/GStatusTags.spec.js index 7f6f5fba05..440cff59b8 100644 --- a/frontend/__tests__/components/GStatusTags.spec.js +++ b/frontend/__tests__/components/GStatusTags.spec.js @@ -17,6 +17,9 @@ import { createShootItemComposable } from '@/composables/useShootItem' const { createVuetifyPlugin } = global.fixtures.helper +const originalRequestAnimationFrame = window.requestAnimationFrame +const originalCancelAnimationFrame = window.cancelAnimationFrame + describe('components', () => { describe('g-status-tags', () => { const lastTransitionTime = 'last-transition-time' @@ -51,6 +54,12 @@ describe('components', () => { } beforeEach(() => { + window.requestAnimationFrame = callback => { + callback() + return 1 + } + window.cancelAnimationFrame = () => {} + pinia = createTestingPinia({ stubActions: false }) const authnStore = useAuthnStore(pinia) // eslint-disable-line no-unused-vars const configStore = useConfigStore(pinia) @@ -75,6 +84,11 @@ describe('components', () => { } }) + afterEach(() => { + window.requestAnimationFrame = originalRequestAnimationFrame + window.cancelAnimationFrame = originalCancelAnimationFrame + }) + it('should generate condition object for simple condition type', () => { const type = 'SampleConditionAvailability' const wrapper = mountStatusTags([type]) @@ -166,5 +180,63 @@ describe('components', () => { const shortNames = wrapper.vm.conditions.map(condition => condition.shortName) expect(shortNames).toEqual(['IC', 'C', 'D', 'CPO']) }) + + it('should auto scroll right when hovering near the right edge', async () => { + const wrapper = mountStatusTags(['APIServerAvailable']) + const scrollContainer = document.createElement('div') + Object.defineProperties(scrollContainer, { + scrollLeft: { + value: 0, + writable: true, + }, + scrollWidth: { + value: 200, + }, + clientWidth: { + value: 100, + }, + }) + scrollContainer.getBoundingClientRect = () => ({ + left: 0, + right: 100, + }) + + wrapper.vm.containerRef = wrapper.element + wrapper.vm.containerRef.closest = vi.fn().mockReturnValue(scrollContainer) + + wrapper.vm.onMouseMove({ clientX: 95 }) + await wrapper.vm.$nextTick() + + expect(scrollContainer.scrollLeft).toBeGreaterThan(0) + }) + + it('should stop auto scroll when cursor moves away from the edge', async () => { + const wrapper = mountStatusTags(['APIServerAvailable']) + const scrollContainer = document.createElement('div') + Object.defineProperties(scrollContainer, { + scrollLeft: { + value: 10, + writable: true, + }, + scrollWidth: { + value: 200, + }, + clientWidth: { + value: 100, + }, + }) + scrollContainer.getBoundingClientRect = () => ({ + left: 0, + right: 100, + }) + + wrapper.vm.containerRef = wrapper.element + wrapper.vm.containerRef.closest = vi.fn().mockReturnValue(scrollContainer) + + wrapper.vm.onMouseMove({ clientX: 50 }) + await wrapper.vm.$nextTick() + + expect(scrollContainer.scrollLeft).toBe(10) + }) }) }) diff --git a/frontend/src/components/GScrollContainer.vue b/frontend/src/components/GScrollContainer.vue index 1ad9edcb3f..f73fef702e 100644 --- a/frontend/src/components/GScrollContainer.vue +++ b/frontend/src/components/GScrollContainer.vue @@ -6,11 +6,22 @@ SPDX-License-Identifier: Apache-2.0 @@ -18,49 +29,119 @@ SPDX-License-Identifier: Apache-2.0 import { ref, computed, + toRefs, } from 'vue' import { useScroll, useElementSize, } from '@vueuse/core' +const props = defineProps({ + direction: { + type: String, + default: 'vertical', + validator: value => ['vertical', 'horizontal'].includes(value), + }, +}) + +const { direction } = toRefs(props) + const scrollRef = ref(null) -const { y } = useScroll(scrollRef) -const { height } = useElementSize(scrollRef) +const contentRef = ref(null) +const { x, y } = useScroll(scrollRef) +const { width, height } = useElementSize(scrollRef) +const { width: contentWidth, height: contentHeight } = useElementSize(contentRef) -const fadeHeight = Math.max(40, height.value / 2) +const fadeSize = computed(() => { + if (direction.value === 'horizontal') { + return Math.max(20, width.value / 4) + } + return Math.max(40, height.value / 2) +}) const fadeOpacity = computed(() => { const el = scrollRef.value if (!el) { - return false + return 0 } - const remainingY = el.scrollHeight - (y.value + height.value) - if (remainingY >= fadeHeight) { + if (direction.value === 'horizontal') { + const remainingX = contentWidth.value - (x.value + width.value) + if (remainingX <= 0) { + return 0 + } + if (remainingX >= fadeSize.value) { + return 1 + } + return remainingX / fadeSize.value + } + + const remainingY = contentHeight.value - (y.value + height.value) + if (remainingY <= 0) { + return 0 + } + if (remainingY >= fadeSize.value) { return 1 } + return remainingY / fadeSize.value +}) + +const maskStyle = computed(() => { + if (fadeOpacity.value <= 0) { + return {} + } - return remainingY / fadeHeight + const effectiveFadeSize = Math.round(fadeSize.value * fadeOpacity.value) + + if (direction.value === 'horizontal') { + const gradient = `linear-gradient(to right, black 0, black calc(100% - ${effectiveFadeSize}px), transparent 100%)` + return { + maskImage: gradient, + WebkitMaskImage: gradient, + } + } + + const gradient = `linear-gradient(to bottom, black 0, black calc(100% - ${effectiveFadeSize}px), transparent 100%)` + return { + maskImage: gradient, + WebkitMaskImage: gradient, + } }) +const wrapperClasses = computed(() => ({ + 'scroll-wrapper--vertical': direction.value === 'vertical', + 'scroll-wrapper--horizontal': direction.value === 'horizontal', +})) + +const containerClasses = computed(() => ({ + 'scrollable-container--vertical': direction.value === 'vertical', + 'scrollable-container--horizontal': direction.value === 'horizontal', +})) diff --git a/frontend/src/components/GSeedStatusTag.vue b/frontend/src/components/GSeedStatusTag.vue index 61a31f2821..4df4b911b0 100644 --- a/frontend/src/components/GSeedStatusTag.vue +++ b/frontend/src/components/GSeedStatusTag.vue @@ -5,7 +5,12 @@ SPDX-License-Identifier: Apache-2.0 -->