Commit d219fb2d authored by Kushal Pandya's avatar Kushal Pandya

Update sidebar app to be an independent component

Updates `issuable_sidebar` app to be independent
component without needing bootstrap script.
parent 6e7e1982
<script>
import Cookies from 'js-cookie';
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
components: {
GlIcon,
},
data() {
const userExpanded = !parseBoolean(Cookies.get('collapsed_gutter'));
// We're deliberately keeping two different props for sidebar status;
// 1. userExpanded reflects value based on cookie `collapsed_gutter`.
// 2. isExpanded reflect actual sidebar state.
return {
userExpanded,
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
};
},
watch: {
isExpanded(expanded) {
this.$emit('sidebar-toggle', {
expanded,
});
},
},
mounted() {
window.addEventListener('resize', this.handleWindowResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleWindowResize);
},
methods: {
updatePageContainerClass() {
const layoutPageEl = document.querySelector('.layout-page');
if (layoutPageEl) {
layoutPageEl.classList.toggle('right-sidebar-expanded', this.isExpanded);
layoutPageEl.classList.toggle('right-sidebar-collapsed', !this.isExpanded);
}
},
handleWindowResize() {
if (this.userExpanded) {
this.isExpanded = bp.isDesktop();
this.updatePageContainerClass();
}
},
handleToggleSidebarClick() {
this.isExpanded = !this.isExpanded;
this.userExpanded = this.isExpanded;
Cookies.set('collapsed_gutter', !this.userExpanded);
this.updatePageContainerClass();
},
},
};
</script>
<template>
<aside
:class="{ 'right-sidebar-expanded': isExpanded, 'right-sidebar-collapsed': !isExpanded }"
class="right-sidebar"
aria-live="polite"
>
<button
class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
:title="__('Toggle sidebar')"
@click="handleToggleSidebarClick"
>
<span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
__('Collapse sidebar')
}}</span>
<gl-icon v-show="isExpanded" data-testid="icon-collapse" name="chevron-double-lg-right" />
<gl-icon
v-show="!isExpanded"
data-testid="icon-expand"
name="chevron-double-lg-left"
class="gl-ml-2"
/>
</button>
<div data-testid="sidebar-items" class="issuable-sidebar">
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded: isExpanded }"></slot>
</div>
</aside>
</template>
<script>
export default {
props: {
signedIn: {
type: Boolean,
required: true,
},
sidebarStatusClass: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<aside
:class="sidebarStatusClass"
class="right-sidebar js-right-sidebar js-issuable-sidebar"
aria-live="polite"
></aside>
</template>
import Vue from 'vue';
import SidebarApp from './components/sidebar_app.vue';
export default () => {
const el = document.getElementById('js-vue-issuable-sidebar');
if (!el) {
return false;
}
const { sidebarStatusClass } = el.dataset;
// An empty string is present when user is signed in.
const signedIn = el.dataset.signedIn === '';
return new Vue({
el,
components: { SidebarApp },
render: createElement =>
createElement('sidebar-app', {
props: {
signedIn,
sidebarStatusClass,
},
}),
});
};
......@@ -10,7 +10,6 @@ import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
export default function() {
......@@ -33,11 +32,7 @@ export default function() {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
if (gon.features && gon.features.vueIssuableSidebar) {
initVueIssuableSidebarApp();
} else {
initIssuableSidebar();
}
initIssuableSidebar();
loadAwardsHandler();
}
......@@ -4,17 +4,12 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler';
export default function() {
new ZenMode(); // eslint-disable-line no-new
if (gon.features && gon.features.vueIssuableSidebar) {
initVueIssuableSidebarApp();
} else {
initIssuableSidebar();
}
initIssuableSidebar();
initPipelines();
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
......
......@@ -363,20 +363,30 @@
// Collapsed nav
.toggle-sidebar-button,
.close-nav-button {
width: $contextual-sidebar-width - 1px;
.close-nav-button,
.toggle-right-sidebar-button {
transition: width $sidebar-transition-duration;
position: fixed;
height: $toggle-sidebar-height;
bottom: 0;
padding: 0 $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
&:hover {
background-color: $border-color;
color: $gl-text-color;
}
}
.toggle-sidebar-button,
.close-nav-button {
position: fixed;
bottom: 0;
width: $contextual-sidebar-width - 1px;
border-top: 1px solid $border-color;
svg {
margin-right: 8px;
}
......@@ -384,11 +394,10 @@
.icon-chevron-double-lg-right {
display: none;
}
}
&:hover {
background-color: $border-color;
color: $gl-text-color;
}
.toggle-right-sidebar-button {
border-bottom: 1px solid $border-color;
}
.collapse-text {
......
......@@ -4,7 +4,6 @@ import { mapState, mapGetters } from 'vuex';
import { PathIdSeparator } from '~/related_issues/constants';
import IssuableBody from '~/issue_show/components/app.vue';
import IssuableSidebar from '~/issuable_sidebar/components/sidebar_app.vue';
import EpicSidebar from './epic_sidebar.vue';
......@@ -12,7 +11,6 @@ export default {
PathIdSeparator,
components: {
IssuableBody,
IssuableSidebar,
EpicSidebar,
},
computed: {
......@@ -33,9 +31,6 @@ export default {
'sidebarCollapsed',
]),
...mapGetters(['isUserSignedIn']),
isVueIssuableEpicSidebarEnabled() {
return gon.features && gon.features.vueIssuableEpicSidebar;
},
sidebarStatusClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
......@@ -67,11 +62,6 @@ export default {
issuable-type="epic"
/>
</div>
<issuable-sidebar
v-if="isVueIssuableEpicSidebarEnabled"
:signed-in="isUserSignedIn"
:sidebar-status-class="sidebarStatusClass"
/>
<epic-sidebar v-else />
<epic-sidebar />
</div>
</template>
import { shallowMount } from '@vue/test-utils';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import IssuableSidebarRoot from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
const createComponent = (expanded = true) =>
shallowMount(IssuableSidebarRoot, {
propsData: {
expanded,
},
slots: {
'right-sidebar-items': `
<button class="js-todo">Todo</button>
`,
},
});
describe('IssuableSidebarRoot', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('watch', () => {
describe('isExpanded', () => {
it('emits `sidebar-toggle` event on component', async () => {
wrapper.setData({
isExpanded: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
expect(wrapper.emitted('sidebar-toggle')[0]).toEqual([
{
expanded: false,
},
]);
});
});
});
describe('methods', () => {
describe('updatePageContainerClass', () => {
beforeEach(() => {
setFixtures('<div class="layout-page"></div>');
});
it.each`
isExpanded | layoutPageClass
${true} | ${'right-sidebar-expanded'}
${false} | ${'right-sidebar-collapsed'}
`(
'set class $layoutPageClass to container element when `isExpanded` prop is $isExpanded',
async ({ isExpanded, layoutPageClass }) => {
wrapper.setData({
isExpanded,
});
await wrapper.vm.$nextTick();
wrapper.vm.updatePageContainerClass();
expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
true,
);
},
);
});
describe('handleWindowResize', () => {
beforeEach(async () => {
wrapper.setData({
userExpanded: true,
});
await wrapper.vm.$nextTick();
});
it.each`
breakpoint | isExpandedValue
${'xs'} | ${false}
${'sm'} | ${false}
${'md'} | ${false}
${'lg'} | ${true}
${'xl'} | ${true}
`(
'sets `isExpanded` prop to $isExpandedValue only when current screen size is `lg` or `xl`',
async ({ breakpoint, isExpandedValue }) => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.isExpanded).toBe(isExpandedValue);
},
);
it('calls `updatePageContainerClass` method', () => {
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
});
});
describe('handleToggleSidebarClick', () => {
beforeEach(async () => {
jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
wrapper.setData({
isExpanded: true,
});
await wrapper.vm.$nextTick();
});
it('flips value of `isExpanded`', () => {
wrapper.vm.handleToggleSidebarClick();
expect(wrapper.vm.isExpanded).toBe(false);
expect(wrapper.vm.userExpanded).toBe(false);
});
it('updates "collapsed_gutter" cookie value', () => {
wrapper.vm.handleToggleSidebarClick();
expect(Cookies.set).toHaveBeenCalledWith('collapsed_gutter', true);
});
it('calls `updatePageContainerClass` method', () => {
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
wrapper.vm.handleWindowResize();
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
});
});
});
describe('template', () => {
describe('sidebar expanded', () => {
beforeEach(async () => {
wrapper.setData({
isExpanded: true,
});
await wrapper.vm.$nextTick();
});
it('renders component container element with class `right-sidebar-expanded` when `isExpanded` prop is true', () => {
expect(wrapper.classes()).toContain('right-sidebar-expanded');
});
it('renders sidebar toggle button with text and icon', () => {
const buttonEl = wrapper.find('button');
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
expect(buttonEl.find('[data-testid="icon-collapse"]').isVisible()).toBe(true);
});
});
describe('sidebar collapsed', () => {
beforeEach(async () => {
wrapper.setData({
isExpanded: false,
});
await wrapper.vm.$nextTick();
});
it('renders component container element with class `right-sidebar-collapsed` when `isExpanded` prop is false', () => {
expect(wrapper.classes()).toContain('right-sidebar-collapsed');
});
it('renders sidebar toggle button with text and icon', () => {
const buttonEl = wrapper.find('button');
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
expect(buttonEl.find('[data-testid="icon-expand"]').isVisible()).toBe(true);
});
});
it('renders sidebar items', () => {
const sidebarItemsEl = wrapper.find('[data-testid="sidebar-items"]');
expect(sidebarItemsEl.exists()).toBe(true);
expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
});
});
});
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