Skip to content

Commit 2a42a32

Browse files
authored
Merge pull request #1043 from AaronDCole/resizable-drawer
allow the left hand drawer to re resized by the user if required
2 parents a0d27ea + d0c3dc8 commit 2a42a32

File tree

5 files changed

+226
-9
lines changed

5 files changed

+226
-9
lines changed

src/components/cylc/Drawer.vue

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
<template>
1919
<v-navigation-drawer
2020
v-model="drawer"
21+
ref="drawerRef"
2122
app
2223
floating
24+
hide-overlay
2325
mobile-breakpoint="991"
24-
width="260"
26+
:width="navigation.width"
2527
persistent
2628
class="fill-height"
2729
>
@@ -99,19 +101,81 @@ export default {
99101
graphql: mdiGraphql
100102
},
101103
environment: process.env.VUE_APP_SERVICES === 'offline' ? 'OFFLINE' : process.env.NODE_ENV.toUpperCase(),
102-
version
104+
version,
105+
navigation: {
106+
width: 260,
107+
borderSize: 3
108+
}
103109
}
104110
},
105111
computed: {
106112
...mapState('user', ['user']),
107113
drawer: {
108-
get: function () {
114+
get () {
109115
return this.$store.state.app.drawer
110116
},
111-
set: function (val) {
117+
set (val) {
118+
if (val) {
119+
const newWidth = typeof this.navigation.width === 'string' ? Number(this.navigation.width.replace('px', '')) : this.navigation.width
120+
this.navigation.width = newWidth < 260 ? 260 : newWidth
121+
}
112122
this.$store.commit('app/setDrawer', val)
113123
}
114124
}
125+
},
126+
methods: {
127+
getDrawerElement () {
128+
return this.$refs.drawerRef.$el
129+
},
130+
setBorderWidth () {
131+
const i = this.getDrawerElement().querySelector(
132+
'.v-navigation-drawer__border'
133+
)
134+
i.style.width = this.navigation.borderSize + 'px'
135+
i.style.cursor = 'ew-resize'
136+
},
137+
resize (e) {
138+
document.body.style.cursor = 'ew-resize'
139+
const el = this.getDrawerElement()
140+
const direction = el.classList.contains('v-navigation-drawer--right')
141+
? 'right'
142+
: 'left'
143+
const f = direction === 'right' ? document.body.scrollWidth - e.clientX : e.clientX
144+
el.style.width = f + 'px'
145+
},
146+
setEvents () {
147+
const el = this.getDrawerElement()
148+
const drawerBorder = el.querySelector('.v-navigation-drawer__border')
149+
drawerBorder.addEventListener(
150+
'mousedown',
151+
(e) => {
152+
el.style.transition = 'initial'
153+
document.addEventListener('mousemove', this.resize, false)
154+
if (e.stopPropagation) e.stopPropagation()
155+
if (e.preventDefault) e.preventDefault()
156+
return false
157+
},
158+
false
159+
)
160+
document.addEventListener(
161+
'mouseup',
162+
() => {
163+
el.style.transition = ''
164+
this.navigation.width = el.style.width
165+
document.body.style.cursor = ''
166+
document.removeEventListener('mousemove', this.resize, false)
167+
// this slightly hacky timeout is used to ensure a browser redraw forced the lumino tabs to be resized when the drag event has finished
168+
setTimeout(() => {
169+
window.dispatchEvent(new Event('resize'))
170+
}, 600)
171+
},
172+
false
173+
)
174+
}
175+
},
176+
mounted () {
177+
this.setBorderWidth()
178+
this.setEvents()
115179
}
116180
}
117181
</script>

src/mixins/toolbar.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ export default {
3232
mounted () {
3333
this.onResponsiveInverted()
3434
window.addEventListener('resize', this.onResponsiveInverted)
35+
document.querySelector('.v-navigation-drawer')?.addEventListener('resize', this.onResponsiveInverted)
3536
},
3637

3738
beforeDestroy () {
3839
window.removeEventListener('resize', this.onResponsiveInverted)
40+
document.querySelector('.v-navigation-drawer')?.removeEventListener('resize', this.onResponsiveInverted)
3941
},
4042

4143
methods: {
@@ -44,7 +46,10 @@ export default {
4446
this.setDrawer(!this.$store.state.app.drawer)
4547
},
4648
onResponsiveInverted () {
47-
if (window.innerWidth < 991) {
49+
if (document.querySelector('.v-navigation-drawer')?.clientWidth < 5) {
50+
this.setDrawer(false)
51+
}
52+
if (window.innerWidth < 991 || document.querySelector('.v-navigation-drawer')?.clientWidth < 5) {
4853
this.responsive = true
4954
this.responsiveInput = false
5055
} else {

tests/e2e/specs/drawer.cy.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ describe('Drawer component', () => {
2222
.get('.v-navigation-drawer')
2323
.should('be.visible')
2424
})
25+
it('it should have a width of 260', () => {
26+
cy.get('.v-navigation-drawer').invoke('innerWidth').should('be.eq', 260)
27+
})
2528
it('Is NOT displayed when mode is mobile', () => {
2629
// when the window dimension is below a mobile-threshold, the app sets state.app.drawer as false
2730
// and then the drawer is hidden
@@ -35,4 +38,15 @@ describe('Drawer component', () => {
3538
.get('#toggle-drawer')
3639
.should('be.visible')
3740
})
41+
it('should drag to trigger resize', () => {
42+
cy.visit('/#/')
43+
cy.get('.v-navigation-drawer').invoke('innerWidth').should('be.eq', 260)
44+
cy.get('.v-navigation-drawer__border')
45+
.trigger('mousedown', { which: 1 })
46+
.trigger('mousemove', { clientX: 0, clientY: 500 })
47+
.trigger('mouseup', { force: true })
48+
cy.get('.v-navigation-drawer')
49+
.invoke('innerWidth').should('be.eq', 0)
50+
cy.get('#toggle-drawer').should('exist')
51+
})
3852
})
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { expect } from 'chai'
19+
import { createLocalVue, mount } from '@vue/test-utils'
20+
import Drawer from '../../../../src/components/cylc/Drawer'
21+
22+
import Vue from 'vue'
23+
import Vuetify from 'vuetify'
24+
import Vuex from 'vuex'
25+
26+
import sinon from 'sinon'
27+
28+
Vue.use(Vuex)
29+
Vue.use(Vuetify)
30+
31+
let vuetify
32+
let wrapper
33+
34+
const mountFunction = options => {
35+
const localVue = createLocalVue()
36+
37+
// note these are truly 'mocked' because I ran into issues with the state being tainted across multiple unit-tests
38+
const store = new Vuex.Store({
39+
modules: {
40+
app: {
41+
namespaced: true,
42+
state: {
43+
drawer: {}
44+
},
45+
mutations: {
46+
setDrawer (state, drawer) {
47+
state.drawer = drawer
48+
}
49+
}
50+
},
51+
user: {
52+
namespaced: true,
53+
state: {
54+
user: {
55+
username: 'test username'
56+
}
57+
},
58+
mutations: {
59+
SET_USER (state, user) {
60+
state.user = user
61+
}
62+
}
63+
}
64+
}
65+
})
66+
67+
vuetify = new Vuetify()
68+
69+
return mount(Drawer, {
70+
localVue,
71+
vuetify,
72+
store,
73+
mocks: {
74+
$t: () => {
75+
}
76+
},
77+
stubs: {
78+
Header: true,
79+
GScan: true,
80+
RouterLink: true
81+
},
82+
...options
83+
})
84+
}
85+
86+
describe('Drawer', () => {
87+
it('should create the drawer and successfully trigger a resize', async () => {
88+
const createBubbledEvent = (type, props = {}) => {
89+
const event = new Event(type, { bubbles: true })
90+
Object.assign(event, props)
91+
return event
92+
}
93+
const sandbox = sinon.createSandbox()
94+
wrapper = mountFunction()
95+
await wrapper.vm.$nextTick()
96+
expect(wrapper.find('div.d-flex.flex-column.h-100').exists()).to.equal(true)
97+
const spyFunction = sandbox.spy(wrapper.vm, 'resize')
98+
99+
wrapper.find('div.v-navigation-drawer__border').element.dispatchEvent(
100+
createBubbledEvent('mousedown', { offsetX: 1 })
101+
)
102+
103+
document.dispatchEvent(
104+
createBubbledEvent('mousemove', { clientX: 100, clientY: 0, offsetX: 50 })
105+
)
106+
107+
document.dispatchEvent(
108+
createBubbledEvent('mouseup', { clientX: 100, clientY: 0, offsetX: 0 })
109+
)
110+
111+
expect(spyFunction.called).to.equal(true)
112+
})
113+
})

tests/unit/components/cylc/toolbar.vue.spec.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import { mount, createLocalVue } from '@vue/test-utils'
1919
import { expect } from 'chai'
2020
import Toolbar from '@/components/cylc/Toolbar'
21-
import storeOptions from '@/store/options'
2221
import Vuetify from 'vuetify/lib'
2322
import WorkflowState from '@/model/WorkflowState.model'
2423
import Vuex from 'vuex'
@@ -31,7 +30,30 @@ Vue.use(Vuex)
3130
describe('Toolbar component', () => {
3231
let vuetify
3332
let $route
34-
const store = new Vuex.Store(storeOptions)
33+
// for some obscene reason, using the actual "options.js" store, gets contaminated data from other tests during the course of the whole test running
34+
// by mocking the entire store, we can decide exactly what data we want to be available within each test, without fear it will be overwritten or tainted with
35+
// by other tests
36+
const store = new Vuex.Store({
37+
modules: {
38+
app: {
39+
namespaced: true,
40+
state: {
41+
drawer: null
42+
}
43+
},
44+
workflows: {
45+
namespaced: true,
46+
state: {
47+
workflow: {
48+
tree: {},
49+
lookup: {}
50+
},
51+
workflowName: null,
52+
workflows: {}
53+
}
54+
}
55+
}
56+
})
3557
const mountFunction = options => {
3658
return mount(Toolbar, {
3759
localVue,
@@ -75,6 +97,7 @@ describe('Toolbar component', () => {
7597
})
7698
it('should hide and display drawer according to screen viewport size', async () => {
7799
const wrapper = mountFunction()
100+
await wrapper.vm.$nextTick()
78101
await wrapper.setData({
79102
responsive: false
80103
})
@@ -86,7 +109,5 @@ describe('Toolbar component', () => {
86109
responsive: true
87110
})
88111
expect(wrapper.find('button.default').exists()).to.equal(true)
89-
wrapper.find('button.default').trigger('click')
90-
expect(store.state.app.drawer).to.equal(true)
91112
})
92113
})

0 commit comments

Comments
 (0)