Skip to content

Commit 3168e93

Browse files
authoredNov 10, 2017
perf(dropdowns): convert templates to render functions (#1314)
* [dropdown mixin] prep for render functions * Create nav-item-dropdown.js * Update index.js * Delete nav-item-dropdown.vue * Update dropdown.js * Update dropdown.js
1 parent 88657fb commit 3168e93

File tree

5 files changed

+265
-112
lines changed

5 files changed

+265
-112
lines changed
 

‎src/components/dropdown/dropdown.js

+119-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,120 @@
1-
import bDropdown from './dropdown.vue';
1+
import { idMixin, dropdownMixin } from '../../mixins';
2+
import bButton from '../button/button';
23

3-
export default bDropdown;
4+
export default {
5+
mixins: [idMixin, dropdownMixin],
6+
components: {bButton},
7+
render(h) {
8+
const t = this;
9+
let split = h(false);
10+
if (t.split) {
11+
split = h(
12+
'b-button',
13+
{
14+
ref: 'button',
15+
props: {
16+
disabled: t.disabled,
17+
variant: t.variant,
18+
size: t.size
19+
},
20+
attrs: {
21+
id: t.safeId('_BV_button_')
22+
},
23+
on: {
24+
click: t.click
25+
}
26+
},
27+
[ t.$slots['button-content'] || t.$slots.text || t.text ]
28+
);
29+
}
30+
const toggle = h(
31+
'b-button',
32+
{
33+
ref: 'toggle',
34+
class: {
35+
'dropdown-toggle': !t.noCaret || t.split,
36+
'dropdown-toggle-split': t.split
37+
},
38+
props: {
39+
variant: t.variant,
40+
size: t.size,
41+
disabled: t.disabled
42+
},
43+
attrs: {
44+
id: t.safeId('_BV_toggle_'),
45+
'aria-haspopup': 'true',
46+
'aria-expanded': t.visible ? 'true' : 'false'
47+
},
48+
on: {
49+
click: t.toggle, // click
50+
keydown: t.toggle // enter, space, down
51+
}
52+
},
53+
[ t.split
54+
? h('span', { class: [ 'sr-only' ] }, [t.toggleText])
55+
: (t.$slots['button-content'] || t.$slots.text || t.text)
56+
]
57+
);
58+
const menu = h(
59+
'div',
60+
{
61+
ref: 'menu',
62+
class: t.menuClasses,
63+
attrs: {
64+
role: t.role,
65+
'aria-labelledby': t.safeId(split ? '_BV_toggle_' : '_BV_button_')
66+
},
67+
on: {
68+
mouseover: t.onMouseOver,
69+
keydown: t.onKeydown // tab, up, down, esc
70+
}
71+
},
72+
[ this.$slots.default ]
73+
);
74+
return h('div', { attrs: { id: t.safeId() }, class: t.dropdownClasses }, [split, toggle, menu]);
75+
},
76+
props: {
77+
split: {
78+
type: Boolean,
79+
default: false
80+
},
81+
toggleText: {
82+
type: String,
83+
default: 'Toggle Dropdown'
84+
},
85+
size: {
86+
type: String,
87+
default: null
88+
},
89+
variant: {
90+
type: String,
91+
default: null
92+
},
93+
noCaret: {
94+
type: Boolean,
95+
default: false,
96+
},
97+
role: {
98+
type: String,
99+
default: 'menu'
100+
}
101+
},
102+
computed: {
103+
dropdownClasses() {
104+
return [
105+
'btn-group',
106+
'b-dropdown',
107+
'dropdown',
108+
this.dropup ? 'dropup' : '',
109+
this.visible ? 'show' : ''
110+
];
111+
},
112+
menuClasses() {
113+
return [
114+
'dropdown-menu',
115+
this.right ? 'dropdown-menu-right' : '',
116+
this.visible ? 'show' : ''
117+
];
118+
}
119+
}
120+
};

‎src/components/nav/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import bNav from './nav';
22
import bNavItem from './nav-item';
33
import bNavText from './nav-text';
44
import bNavForm from './nav-form';
5-
import bNavItemDropdown from './nav-item-dropdown.vue';
5+
import bNavItemDropdown from './nav-item-dropdown';
66
import dropdownPlugin from '../dropdown';
77
import { registerComponents, vueUse } from '../../utils';
88

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { idMixin, dropdownMixin } from '../../mixins';
2+
3+
export default {
4+
mixins: [idMixin, dropdownMixin],
5+
render(h) {
6+
const t = this;
7+
const button = h(
8+
'a',
9+
{
10+
class: t.toggleClasses,
11+
ref: 'toggle',
12+
attrs: {
13+
href: '#',
14+
id: t.safeId('_BV_button_'),
15+
disabled: t.disabled,
16+
'aria-haspopup': 'true',
17+
'aria-expanded': t.visible ? 'true' : 'false'
18+
},
19+
on: {
20+
click: t.toggle,
21+
keydown: t.toggle // space, enter, down
22+
}
23+
},
24+
[ t.$slots['button-content'] || t.$slots.text || h('span', { domProps: { innerHTML: t.text } }) ]
25+
);
26+
const menu = h(
27+
'div',
28+
{
29+
class: t.menuClasses,
30+
ref: 'menu',
31+
attrs: { 'aria-labelledby': t.safeId('_BV_button_') },
32+
on: {
33+
mouseover: t.onMouseOver,
34+
keydown: t.onKeydown // tab, up, down, esc
35+
}
36+
},
37+
[ this.$slots.default ]
38+
);
39+
return h('li', { attrs: { id: t.safeId() }, class: t.dropdownClasses }, [ button, menu ]);
40+
},
41+
computed: {
42+
isNav() {
43+
// Signal to dropdown mixin that we are in a navbar
44+
return true;
45+
},
46+
dropdownClasses() {
47+
return [
48+
'nav-item',
49+
'b-nav-dropdown',
50+
'dropdown',
51+
this.dropup ? 'dropup' : '',
52+
this.visible ? 'show' : ''
53+
];
54+
},
55+
toggleClasses() {
56+
return [
57+
'nav-link',
58+
this.noCaret ? '' : 'dropdown-toggle',
59+
this.disabled ? 'disabled' : ''
60+
];
61+
},
62+
menuClasses() {
63+
return [
64+
'dropdown-menu',
65+
this.right ? 'dropdown-menu-right': 'dropdown-menu-left',
66+
this.visible ? 'show' : ''
67+
];
68+
}
69+
},
70+
props: {
71+
noCaret: {
72+
type: Boolean,
73+
default: false
74+
},
75+
role: {
76+
type: String,
77+
default: 'menu'
78+
}
79+
}
80+
};

‎src/components/nav/nav-item-dropdown.vue

-80
This file was deleted.

‎src/mixins/dropdown.js

+65-29
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ const AttachmentMap = {
2626
BOTTOMEND: "bottom-end"
2727
};
2828

29+
// Keyboard keys
30+
const KEY = {
31+
ENTER: 13,
32+
SPACE: 32,
33+
TAB: 9,
34+
DOWN: 40,
35+
UP: 38,
36+
ESC: 27
37+
};
38+
2939
export default {
3040
mixins: [clickoutMixin, listenOnRootMixin],
3141
props: {
@@ -71,8 +81,8 @@ export default {
7181
};
7282
},
7383
created() {
74-
const listener = el => {
75-
if (el !== this) {
84+
const listener = vm => {
85+
if (vm !== this) {
7686
this.visible = false;
7787
}
7888
};
@@ -213,52 +223,78 @@ export default {
213223
clickOutListener() {
214224
this.visible = false;
215225
},
216-
click(e) {
217-
// Calle only in split button mode, for the split button
226+
show() {
227+
// Public method to show dropdown
218228
if (this.disabled) {
219-
this.visible = false;
220229
return;
221230
}
222-
223-
this.$emit("click", e);
231+
this.visible = true;
224232
},
225-
toggle() {
233+
hide() {
234+
// Public method to hide dropdown
235+
if (this.disabled) {
236+
return;
237+
}
238+
this.visible = false;
239+
},
240+
toggle(evt) {
226241
// Called only by a button that toggles the menu
242+
evt = evt || {};
243+
const type = evt.type;
244+
const key = evt.keyCode;
245+
if (type !== "click" && !(type === "keydown" && (key === KEY.ENTER || key === KEY.SPACE || key === KEY.DOWN))) {
246+
// We only toggle on Click, Enter, Space, and Arrow Down
247+
return;
248+
}
249+
evt.preventDefault();
250+
evt.stopPropagation();
227251
if (this.disabled) {
228252
this.visible = false;
229253
return;
230254
}
255+
// Toggle visibility
231256
this.visible = !this.visible;
232257
},
233-
show() {
234-
// Public method to show dropdown
258+
click(evt) {
259+
// Calle only in split button mode, for the split button
235260
if (this.disabled) {
261+
this.visible = false;
236262
return;
237263
}
238-
this.visible = true;
264+
this.$emit("click", evt);
239265
},
240-
hide() {
241-
// Public method to hide dropdown
242-
if (this.disabled) {
243-
return;
266+
onKeydown(evt) {
267+
// Called from dropdown menu context
268+
const key = evt.keyCode;
269+
if (key === KEY.ESC) {
270+
// Close on ESC
271+
this.onEsc(evt);
272+
} else if (key === KEY.TAB) {
273+
// Close on tab out
274+
this.onTab(evt);
275+
} else if (key === KEY.DOWN) {
276+
// Down Arrow
277+
this.focusNext(evt, false);
278+
} else if (key === KEY.UP) {
279+
// Up Arrow
280+
this.focusNext(evt, true);
244281
}
245-
this.visible = false;
246282
},
247-
onTab() {
283+
onEsc(evt) {
248284
if (this.visible) {
249-
// TODO: Need special handler for dealing with form inputs
250-
// Tab, if in a text-like input, we should just focus next item in the dropdown
251-
// Note: Inputs are in a special .dropdown-form container
252285
this.visible = false;
286+
evt.preventDefault();
287+
evt.stopPropagation();
288+
// Return focus to original trigger button
289+
this.$nextTick(this.focusToggler);
253290
}
254291
},
255-
onEsc(e) {
292+
onTab(evt) {
256293
if (this.visible) {
294+
// TODO: Need special handler for dealing with form inputs
295+
// Tab, if in a text-like input, we should just focus next item in the dropdown
296+
// Note: Inputs are in a special .dropdown-form container
257297
this.visible = false;
258-
e.preventDefault();
259-
e.stopPropagation();
260-
// Return focus to original trigger button
261-
this.$nextTick(this.focusToggler);
262298
}
263299
},
264300
onFocusOut(evt) {
@@ -280,18 +316,18 @@ export default {
280316
item.focus();
281317
}
282318
},
283-
focusNext(e, up) {
319+
focusNext(evt, up) {
284320
if (!this.visible) {
285321
return;
286322
}
287-
e.preventDefault();
288-
e.stopPropagation();
323+
evt.preventDefault();
324+
evt.stopPropagation();
289325
this.$nextTick(() => {
290326
const items = this.getItems();
291327
if (items.length < 1) {
292328
return;
293329
}
294-
let index = items.indexOf(e.target);
330+
let index = items.indexOf(evt.target);
295331
if (up && index > 0) {
296332
index--;
297333
} else if (!up && index < items.length - 1) {

0 commit comments

Comments
 (0)
Please sign in to comment.