Skip to content

Commit 80b0d6c

Browse files
authored
Add alignment options to Menu component (#50)
1 parent d6f52d9 commit 80b0d6c

File tree

3 files changed

+124
-43
lines changed

3 files changed

+124
-43
lines changed

components/src/stories/Menu.stories.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ export const Component = {
1010
setup() {
1111
return {args};
1212
},
13-
template: `<ui-menu>
14-
<ui-button
15-
slot="trigger"
16-
text="open menu"
17-
/>
18-
<div style="padding:8px 16px; width:300px; border:1px solid black;" slot="content">
19-
<p>item</p>
20-
</div>
21-
</ui-menu>`
13+
template: `
14+
<ui-menu v-bind="args">
15+
<ui-button
16+
slot="trigger"
17+
text="open menu"
18+
/>
19+
<div style="padding:8px 16px; width:300px; border:1px solid black;" slot="content">
20+
<p>item</p>
21+
</div>
22+
</ui-menu>
23+
`
2224
}),
25+
26+
args: {
27+
align: 'left',
28+
},
2329
};
2430

2531
export default {
@@ -28,4 +34,13 @@ export default {
2834
parameters: {
2935
layout: 'centered',
3036
},
31-
};
37+
38+
argTypes: {
39+
align: {
40+
options: ['right', 'left'],
41+
control: {
42+
type: 'select',
43+
},
44+
},
45+
},
46+
};

components/src/widgets/menu/widget.spec.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,70 @@ describe('Menu component', () => {
4848
describe('onMounted', () => {
4949
it('adds up event listener on component mount', () => {
5050
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
51-
51+
5252
mount(Menu);
5353

5454
expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
55-
55+
5656
addEventListenerSpy.mockRestore();
5757
});
5858
});
5959

6060
describe('onUnmounted', () => {
6161
it('cleans up event listener on component unmount', async () => {
6262
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
63-
63+
6464
const wrapper = mount(Menu);
6565
await wrapper.unmount();
66-
66+
6767
expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
68-
68+
6969
removeEventListenerSpy.mockRestore();
7070
});
71-
})
71+
});
72+
73+
describe('alignment class', () => {
74+
it('sets the "menu-content_align-right" class if align=right', async () => {
75+
const wrapper = mount(Menu, {
76+
props: {
77+
align: 'right',
78+
},
79+
});
80+
81+
// Open menu
82+
await wrapper.find('.menu-trigger').trigger('click');
83+
84+
expect(wrapper.find('.menu-content_align-right').exists()).toEqual(true);
85+
});
86+
87+
it('sets the "menu-content_align-left" class if align=left', async () => {
88+
const wrapper = mount(Menu, {
89+
props: {
90+
align: 'left',
91+
},
92+
});
93+
94+
// Open menu
95+
await wrapper.find('.menu-trigger').trigger('click');
96+
97+
expect(wrapper.find('.menu-content_align-left').exists()).toEqual(true);
98+
});
99+
});
100+
101+
describe('align prop validator', () => {
102+
it.each([
103+
// expected, value
104+
[true, 'left'],
105+
[true, 'right'],
106+
[false, 'center'],
107+
[false, 'foo'],
108+
])(
109+
'returns %s if the prop value is %s',
110+
(expected, value) => {
111+
const result = Menu.props.align.validator(value);
112+
113+
expect(result).toEqual(expected);
114+
},
115+
);
116+
});
72117
});

components/src/widgets/menu/widget.vue

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
ref="menu"
44
class="menu"
55
>
6-
<div
7-
class="menu-trigger"
6+
<div
7+
class="menu-trigger"
88
@click.stop="toggle"
99
>
1010
<slot name="trigger" />
1111
</div>
12-
12+
1313
<div class="menu-content-wrapper">
14-
<div
14+
<div
1515
v-if="showMenu"
1616
class="menu-content"
17+
:class="alignmentClass"
1718
@click.stop
1819
>
1920
<slot name="content" />
@@ -23,39 +24,59 @@
2324
</template>
2425

2526
<script setup>
26-
import { onMounted, onUnmounted, ref } from 'vue'
27+
import { onMounted, onUnmounted, ref, computed } from 'vue'
2728
28-
const showMenu = ref(false)
29-
const menu = ref(null)
29+
const props = defineProps({
30+
align: {
31+
type: String,
32+
default: 'left',
33+
validator(value) {
34+
return ['left', 'right'].includes(value);
35+
},
36+
},
37+
});
3038
31-
const toggle = () => {
32-
showMenu.value = !showMenu.value;
33-
}
39+
const showMenu = ref(false);
40+
const menu = ref(null);
41+
42+
const alignmentClass = computed(() => (props.align === 'left'
43+
? 'menu-content_align-left'
44+
: 'menu-content_align-right'
45+
));
3446
35-
const handleClickOutside = (event) => {
36-
if (menu.value && !menu.value.contains(event.target)) {
37-
showMenu.value = false;
38-
}
47+
const toggle = () => {
48+
showMenu.value = !showMenu.value;
49+
};
50+
51+
const handleClickOutside = (event) => {
52+
if (menu.value && !menu.value.contains(event.target)) {
53+
showMenu.value = false;
3954
}
55+
};
4056
41-
onMounted(() => {
42-
document.addEventListener("click", handleClickOutside)
43-
})
57+
onMounted(() => {
58+
document.addEventListener("click", handleClickOutside);
59+
});
4460
45-
onUnmounted(() => {
46-
document.removeEventListener("click", handleClickOutside)
47-
})
61+
onUnmounted(() => {
62+
document.removeEventListener("click", handleClickOutside);
63+
});
4864
</script>
4965

5066
<style lang="stylus" scoped>
67+
.menu-content-wrapper {
68+
position: relative;
69+
}
5170
52-
.menu-content-wrapper {
53-
position: relative;
54-
}
71+
.menu-content {
72+
position: absolute;
73+
top: 0;
5574
56-
.menu-content {
57-
position: absolute;
58-
top: 0;
75+
&_align-right {
76+
right: 0;
77+
}
78+
&_align-left {
5979
left: 0;
6080
}
61-
</style>
81+
}
82+
</style>

0 commit comments

Comments
 (0)