Commit abba4521 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett Committed by Clement Ho

Resolve "No clues to indicate the list in Go to board dropdown is scrollable"

parent 79350ba8
import Vue from 'vue';
import { throttle } from 'underscore';
import BoardForm from './board_form.vue';
(() => {
......@@ -10,6 +11,7 @@ import BoardForm from './board_form.vue';
Store.createNewListDropdownData();
gl.issueBoards.BoardsSelector = Vue.extend({
name: 'boards-selector',
components: {
BoardForm,
},
......@@ -22,13 +24,22 @@ import BoardForm from './board_form.vue';
type: String,
required: true,
},
throttleDuration: {
type: Number,
default: 200,
},
},
data() {
return {
open: false,
loading: true,
hasScrollFade: false,
scrollFadeInitialized: false,
boards: [],
state: Store.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
};
},
watch: {
......@@ -60,6 +71,11 @@ import BoardForm from './board_form.vue';
showDelete() {
return this.boards.length > 1;
},
scrollFadeClass() {
return {
'fade-out': !this.hasScrollFade,
};
},
},
methods: {
showPage(page) {
......@@ -81,11 +97,32 @@ import BoardForm from './board_form.vue';
this.loading = false;
this.boards = json;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(this.setScrollFade)
.catch(() => {
this.loading = false;
});
}
},
isScrolledUp() {
const { content } = this.$refs;
const currentPosition = this.contentClientHeight + content.scrollTop;
return content && currentPosition < this.maxPosition;
},
initScrollFade() {
this.scrollFadeInitialized = true;
const { content } = this.$refs;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
setScrollFade() {
if (!this.scrollFadeInitialized) this.initScrollFade();
this.hasScrollFade = this.isScrolledUp();
},
},
created() {
this.state.currentBoard = this.currentBoard;
......
......@@ -608,7 +608,7 @@
}
.dropdown-content {
max-height: 215px;
max-height: $dropdown-max-height;
overflow-y: auto;
}
......@@ -1059,3 +1059,28 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
margin: 0;
}
}
.dropdown-content-faded-mask {
position: relative;
.dropdown-list {
max-height: $dropdown-max-height;
overflow-y: auto;
position: relative;
}
&::after {
height: $dropdown-fade-mask-height;
width: 100%;
position: absolute;
bottom: 0;
background: linear-gradient(to top, $white-light 0, rgba($white-light, 0));
transition: opacity $fade-mask-transition-duration $fade-mask-transition-curve;
content: '';
pointer-events: none;
}
&.fade-out::after {
opacity: 0;
}
}
......@@ -344,6 +344,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
$dropdown-max-height: 215px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
......@@ -360,6 +361,7 @@ $dropdown-loading-bg: rgba(#fff, .6);
$dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker;
$dropdown-fade-mask-height: 32px;
/*
* Filtered Search
......@@ -571,6 +573,8 @@ $label-border-radius: 100px;
* Animation
*/
$fade-in-duration: 200ms;
$fade-mask-transition-duration: .1s;
$fade-mask-transition-curve: ease-in-out;
/*
* Lint
......
---
title: Add fade mask to the bottom of the boards selector dropdown list if it can
be scrolled down
merge_request: 3384
author:
type: other
......@@ -2,25 +2,26 @@
- milestone_filter_opts = { format: :json }
- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
%boards-selector{ "inline-template" => true,
%boards-selector.js-boards-selector{ "inline-template" => true,
":current-board" => current_board_json,
"milestone-path" => milestones_filter_path(milestone_filter_opts) }
%span.boards-selector-wrapper
%span.boards-selector-wrapper.js-boards-selector-wrapper
.dropdown
%button.dropdown-menu-toggle{ "v-on:click" => "loadBoards",
%button.dropdown-menu-toggle.js-dropdown-toggle{ "v-on:click" => "loadBoards",
data: { toggle: "dropdown" } }
{{ board.name }}
= icon("chevron-down")
.dropdown-menu{ ":class" => "{ 'is-loading': loading }" }
.dropdown-content
%ul{ "v-if" => "!loading" }
%li{ "v-for" => "board in boards" }
.dropdown-content-faded-mask.js-scroll-fade{ ":class" => "scrollFadeClass" }
%ul.dropdown-list.js-dropdown-list{ "v-if" => "!loading", "v-on:scroll.passive" => "throttledSetScrollFade", ref: "content" }
%li.dropdown-item.js-dropdown-item{ "v-for" => "board in boards" }
%a{ ":href" => "'#{board_base_url}/' + board.id" }
{{ board.name }}
- if !multiple_boards_available? && current_board_parent.boards.size > 1
%li
.small.unclickable
Some of your boards are hidden, activate a license to see them again.
.dropdown-loading{ "v-if" => "loading" }
= icon("spin spinner")
......
/* global BoardService */
import Vue from 'vue';
import BoardService from '~/boards/services/board_service';
import '~/boards/components/boards_selector';
import setTimeoutPromiseHelper from '../../helpers/set_timeout_promise_helper';
import mountComponent from '../../helpers/vue_mount_component_helper';
const throttleDuration = 1;
function waitForScroll() {
return Vue.nextTick()
.then(() => setTimeoutPromiseHelper(throttleDuration))
.then(() => Vue.nextTick());
}
describe('BoardsSelector', () => {
let vm;
let scrollContainer;
let scrollFade;
let boardServiceResponse;
const boards = new Array(20).fill()
.map((board, id) => {
const name = `board${id}`;
return {
id,
name,
};
});
beforeEach((done) => {
loadFixtures('boards/show.html.raw');
window.gl = window.gl || {};
window.gl.boardService = new BoardService({
boardsEndpoint: '',
listsEndpoint: '',
bulkUpdatePath: '',
boardId: '',
});
boardServiceResponse = Promise.resolve({
json() {
return boards;
},
});
spyOn(BoardService.prototype, 'allBoards').and.returnValue(boardServiceResponse);
vm = mountComponent(gl.issueBoards.BoardsSelector, {
throttleDuration,
currentBoard: {},
milestonePath: '',
}, document.querySelector('.js-boards-selector'));
vm.$el.querySelector('.js-dropdown-toggle').click();
boardServiceResponse
.then(() => vm.$nextTick())
.then(() => {
scrollFade = vm.$el.querySelector('.js-scroll-fade');
scrollContainer = scrollFade.querySelector('.js-dropdown-list');
scrollContainer.style.maxHeight = '100px';
scrollContainer.style.overflowY = 'scroll';
})
.then(done)
.catch(done.fail);
});
afterEach(() => {
vm.$destroy();
window.gl.boardService = undefined;
});
it('shows the scroll fade if isScrolledUp', (done) => {
scrollContainer.scrollTop = 0;
waitForScroll()
.then(() => {
expect(scrollFade.classList.contains('fade-out')).toEqual(false);
})
.then(done)
.catch(done.fail);
});
it('hides the scroll fade if not isScrolledUp', (done) => {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
waitForScroll()
.then(() => {
expect(scrollFade.classList.contains('fade-out')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment