-
Notifications
You must be signed in to change notification settings - Fork 133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support sub-menu for dropdown #1455
Changes from 6 commits
57a8329
d0942f5
1fd3231
be946d0
7164878
a26f606
969a96f
e6fa314
4bc69a2
bcb7af3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,13 @@ | |
</ul> | ||
</slot> | ||
</li> | ||
<submenu v-else-if="isSubmenu" ref="submenu"> | ||
<slot slot="button" name=button></slot> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could do
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Updated :) |
||
<slot slot="_header" name="_header"></slot> | ||
<slot slot="header" name="header"></slot> | ||
<slot slot="dropdown-submenu" name="dropdown-menu"></slot> | ||
<slot></slot> | ||
</submenu> | ||
<div v-else ref="dropdown" :class="classes"> | ||
<slot name="before"></slot> | ||
<slot name="button"> | ||
|
@@ -72,6 +79,7 @@ export default { | |
return toBoolean(this.disabled); | ||
}, | ||
isLi () { return this.$parent._navbar || this.$parent.menu || this.$parent._tabset }, | ||
isSubmenu() { return this.$parent._dropdown || this.$parent._submenu }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. simple example where provide / inject could be used to remove the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the clarification. Have added this in :) |
||
menu () { | ||
return !this.$parent || this.$parent.navbar | ||
}, | ||
|
@@ -112,7 +120,10 @@ export default { | |
showDropdownMenu() { | ||
this.show = true; | ||
$(this.$refs.dropdown).findChildren('ul').each(ul => ul.classList.toggle('show', true)); | ||
}, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's correct these in another PR; we could enable linting for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright sure will open another PR for this 👍 |
||
}, | ||
created() { | ||
this._dropdown = true; | ||
}, | ||
mounted () { | ||
const $el = $(this.$refs.dropdown) | ||
|
@@ -130,7 +141,10 @@ export default { | |
} | ||
return false | ||
}) | ||
$el.findChildren('ul').on('click', 'li>a', e => { this.hideDropdownMenu() }) | ||
$el.findChildren('ul').on('click', 'li>a', e => { | ||
if (e.target.classList.contains('submenu-toggle')) { return } | ||
this.hideDropdownMenu(); | ||
}) | ||
}, | ||
beforeDestroy () { | ||
const $el = $(this.$refs.dropdown) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -126,9 +126,19 @@ export default { | |
isExact(url, href) { | ||
return normalizeUrl(url) === normalizeUrl(href); | ||
}, | ||
addClassIfDropdown(dropdownLinks, a) { | ||
addClassIfDropdown(dropdownLinks, a, li) { | ||
if (dropdownLinks.includes(a)) { | ||
a.classList.add('dropdown-current'); | ||
this.addClassIfSubmenu(a, li); | ||
} | ||
}, | ||
addClassIfSubmenu(a, li) { | ||
let el = a.parentElement; | ||
while (el != li) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Updated :) |
||
if (el.classList.contains('dropdown-submenu')) { | ||
$(el).findChildren('a').each(a => a.classList.add('dropdown-current')); | ||
} | ||
el = el.parentElement; | ||
} | ||
}, | ||
highlightLink(url) { | ||
|
@@ -148,7 +158,7 @@ export default { | |
// terminate early on an exact match | ||
if (this.isExact(url, a.href)) { | ||
li.classList.add('current'); | ||
this.addClassIfDropdown(dropdownLinks, a); | ||
this.addClassIfDropdown(dropdownLinks, a, li); | ||
return; | ||
} | ||
} | ||
|
@@ -167,19 +177,19 @@ export default { | |
if (hlMode === 'sibling-or-child') { | ||
if (this.isSibling(url, a.href) || this.isChild(url, a.href)) { | ||
li.classList.add('current'); | ||
this.addClassIfDropdown(dropdownLinks, a); | ||
this.addClassIfDropdown(dropdownLinks, a, li); | ||
return; | ||
} | ||
} else if (hlMode === 'sibling') { | ||
if (this.isSibling(url, a.href)) { | ||
li.classList.add('current'); | ||
this.addClassIfDropdown(dropdownLinks, a); | ||
this.addClassIfDropdown(dropdownLinks, a, li); | ||
return; | ||
} | ||
} else if (hlMode === 'child') { | ||
if (this.isChild(url, a.href)) { | ||
li.classList.add('current'); | ||
this.addClassIfDropdown(dropdownLinks, a); | ||
this.addClassIfDropdown(dropdownLinks, a, li); | ||
return; | ||
} | ||
} else { | ||
|
@@ -209,6 +219,7 @@ export default { | |
}); | ||
}); | ||
$(this.$el).on('click', 'li:not(.dropdown)>a', (e) => { | ||
if (e.target.classList.contains('submenu-toggle')) { return } | ||
setTimeout(() => { this.collapsed = true; }, 200); | ||
}).onBlur((e) => { | ||
if (!this.$el.contains(e.target)) { this.collapsed = true; } | ||
|
@@ -241,7 +252,7 @@ export default { | |
} | ||
|
||
>>> .dropdown-current { | ||
color: #fff; | ||
color: #fff !important; | ||
background: #007bff; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
<template> | ||
<li ref="submenu" :class="classes"> | ||
<slot name="button"> | ||
<a | ||
class="submenu-toggle" | ||
role="button" | ||
:class="{disabled: disabled}" | ||
> | ||
<slot name="_header"> | ||
<slot name="header"></slot> | ||
</slot> | ||
</a> | ||
</slot> | ||
<slot name="dropdown-menu"> | ||
<ul class="dropdown-menu"> | ||
<slot></slot> | ||
</ul> | ||
</slot> | ||
</li> | ||
</template> | ||
|
||
<script> | ||
import { toBoolean } from './utils/utils'; | ||
import $ from './utils/NodeList'; | ||
import positionSubmenu from './utils/submenu'; | ||
|
||
export default { | ||
props: { | ||
show: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
'class': null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Updated :) |
||
addClass: { | ||
type: String, | ||
default: '', | ||
}, | ||
}, | ||
data() { | ||
return { | ||
dropright: true, | ||
dropleft: false, | ||
}; | ||
}, | ||
computed: { | ||
classes() { | ||
return [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's place this directly in the template only for consistency with other components' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright. Updated :) |
||
this.class, this.addClass, 'dropdown-submenu', | ||
{ 'dropright': this.dropright, 'dropleft': this.dropleft }, | ||
]; | ||
}, | ||
disabledBool() { | ||
return toBoolean(this.disabled); | ||
}, | ||
showBool() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this (could replace with just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Replaced all |
||
return toBoolean(this.show); | ||
}, | ||
slots() { | ||
return this.$slots.default; | ||
}, | ||
}, | ||
methods: { | ||
hideSubmenu() { | ||
this.show = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated :) |
||
$(this.$refs.submenu).findChildren('ul').each(ul => ul.classList.toggle('show', false)); | ||
this.alignMenuRight(); | ||
}, | ||
showSubmenu() { | ||
this.show = true; | ||
$(this.$refs.submenu).findChildren('ul').each((ul) => { | ||
ul.classList.toggle('show', true); | ||
if (positionSubmenu.isRightAlign(ul)) { | ||
this.alignMenuRight(); | ||
} else { | ||
this.alignMenuLeft(); | ||
} | ||
positionSubmenu.preventOverflow(ul); | ||
}); | ||
}, | ||
alignMenuRight() { | ||
this.dropright = true; | ||
this.dropleft = false; | ||
}, | ||
alignMenuLeft() { | ||
this.dropright = false; | ||
this.dropleft = true; | ||
}, | ||
}, | ||
created() { | ||
this._submenu = true; | ||
}, | ||
mounted() { | ||
const $el = $(this.$refs.submenu); | ||
if (this.show) { | ||
this.showSubmenu(); | ||
} | ||
$el.onBlur(() => { this.hideSubmenu(); }, false); | ||
$el.findChildren('a,button').on('click', (e) => { | ||
e.preventDefault(); | ||
if (this.disabledBool) { return false; } | ||
if (this.showBool) { | ||
this.hideSubmenu(); | ||
} else { | ||
this.showSubmenu(); | ||
} | ||
return false; | ||
}); | ||
$el.findChildren('a,button').on('mouseover', (e) => { | ||
e.preventDefault(); | ||
if (window.innerWidth > 768) { | ||
e.target.click(); | ||
} | ||
}); | ||
}, | ||
beforeDestroy() { | ||
const $el = $(this.$refs.submenu); | ||
$el.offBlur(); | ||
$el.findChildren('a,button').off(); | ||
$el.findChildren('ul').off(); | ||
}, | ||
}; | ||
</script> | ||
|
||
<style scoped> | ||
.dropdown-submenu { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's indent these to fit the stylelint rules too (they've never been validated in our scripts for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated :) |
||
color: #212529 !important; | ||
padding: 0 !important; | ||
position: relative; | ||
} | ||
|
||
@media (max-width: 767px) { | ||
.dropdown-submenu > ul { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm can this be moved to the same media query below? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Updated :) |
||
padding-bottom: 0; | ||
border-radius: 0; | ||
margin: -.05rem; | ||
} | ||
} | ||
|
||
.submenu-toggle { | ||
display: inline-block; | ||
width: 100%; | ||
padding: .25rem .75rem .25rem 1.5rem; | ||
} | ||
|
||
@media (min-width: 768px) { | ||
.submenu-toggle:after { | ||
display: inline-block; | ||
width: 0; | ||
height: 0; | ||
vertical-align: .255em; | ||
content: ""; | ||
border-top: .3em solid transparent; | ||
border-right: 0; | ||
border-bottom: .3em solid transparent; | ||
border-left: .3em solid; | ||
float: right; | ||
margin-top: .5em; | ||
} | ||
} | ||
|
||
@media (max-width: 767px) { | ||
.submenu-toggle:after { | ||
display: inline-block; | ||
width: 0; | ||
height: 0; | ||
margin-left: .255em; | ||
vertical-align: .255em; | ||
content: ""; | ||
border-top: .3em solid; | ||
border-right: .3em solid transparent; | ||
border-bottom: 0; | ||
border-left: .3em solid transparent; | ||
float: right; | ||
margin-top: .5em; | ||
} | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { mount } from '@vue/test-utils'; | ||
import Dropdown from '../Dropdown.vue'; | ||
import Submenu from '../Submenu.vue'; | ||
|
||
const DEFAULT_STUBS = { | ||
'dropdown': Dropdown, | ||
'submenu': Submenu, | ||
}; | ||
|
||
const DROPDOWN = ` | ||
<li><a href="#" class="dropdown-item">Dropdown Item 1</a></li> | ||
<li><a href="#" class="dropdown-item">Dropdown Item 2</a></li> | ||
<li><a href="#" class="dropdown-item">Dropdown Item 3</a></li> | ||
`; | ||
|
||
const FIRST_LEVEL_NESTED_DROPDOWN = ` | ||
${DROPDOWN} | ||
<dropdown haeder="Submenu Level 1"> | ||
<li><a href="#" class="dropdown-item">Nested Dropdown Item 1</a></li> | ||
<li><a href="#" class="dropdown-item">Nested Dropdown Item 2</a></li> | ||
</dropdown> | ||
`; | ||
|
||
const SECOND_LEVEL_NESTED_DROPDOWN = ` | ||
<dropdown haeder="Submenu Level 1"> | ||
<li><a href="#" class="dropdown-item">Nested Dropdown Item 1</a></li> | ||
<li><a href="#" class="dropdown-item">Nested Dropdown Item 2</a></li> | ||
<dropdown haeder="Submenu Level 2"> | ||
<li><a href="#" class="dropdown-item">Nested Submenu Item</a></li> | ||
</dropdown> | ||
</dropdown> | ||
`; | ||
|
||
describe('Dropdown with submenu', () => { | ||
test('dropdown', async () => { | ||
const wrapper = mount(Dropdown, { | ||
slots: { | ||
header: 'Test Dropdown', | ||
default: DROPDOWN, | ||
}, | ||
stubs: DEFAULT_STUBS, | ||
}); | ||
|
||
expect(wrapper.element).toMatchSnapshot(); | ||
}); | ||
|
||
test('dropdown with one level of submenu', async () => { | ||
const wrapper = mount(Dropdown, { | ||
slots: { | ||
header: 'Test Dropdown', | ||
default: FIRST_LEVEL_NESTED_DROPDOWN, | ||
}, | ||
stubs: DEFAULT_STUBS, | ||
}); | ||
|
||
expect(wrapper.element).toMatchSnapshot(); | ||
}); | ||
|
||
test('dropdown with two levels of submenu', async () => { | ||
const wrapper = mount(Dropdown, { | ||
slots: { | ||
header: 'Test Dropdown', | ||
default: SECOND_LEVEL_NESTED_DROPDOWN, | ||
}, | ||
stubs: DEFAULT_STUBS, | ||
}); | ||
|
||
expect(wrapper.element).toMatchSnapshot(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated :)