Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions components/src/stories/Menu.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,22 @@ export const Component = {
setup() {
return {args};
},
template: `<ui-menu>
<ui-button
slot="trigger"
text="open menu"
/>
<div style="padding:8px 16px; width:300px; border:1px solid black;" slot="content">
<p>item</p>
</div>
</ui-menu>`
template: `
<ui-menu v-bind="args">
<ui-button
slot="trigger"
text="open menu"
/>
<div style="padding:8px 16px; width:300px; border:1px solid black;" slot="content">
<p>item</p>
</div>
</ui-menu>
`
}),

args: {
align: 'left',
},
};

export default {
Expand All @@ -28,4 +34,13 @@ export default {
parameters: {
layout: 'centered',
},
};

argTypes: {
align: {
options: ['right', 'left'],
control: {
type: 'select',
},
},
},
};
57 changes: 51 additions & 6 deletions components/src/widgets/menu/widget.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,70 @@ describe('Menu component', () => {
describe('onMounted', () => {
it('adds up event listener on component mount', () => {
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');

mount(Menu);

expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));

addEventListenerSpy.mockRestore();
});
});

describe('onUnmounted', () => {
it('cleans up event listener on component unmount', async () => {
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');

const wrapper = mount(Menu);
await wrapper.unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));

removeEventListenerSpy.mockRestore();
});
})
});

describe('alignment class', () => {
it('sets the "menu-content_align-right" class if align=right', async () => {
const wrapper = mount(Menu, {
props: {
align: 'right',
},
});

// Open menu
await wrapper.find('.menu-trigger').trigger('click');

expect(wrapper.find('.menu-content_align-right').exists()).toEqual(true);
});

it('sets the "menu-content_align-left" class if align=left', async () => {
const wrapper = mount(Menu, {
props: {
align: 'left',
},
});

// Open menu
await wrapper.find('.menu-trigger').trigger('click');

expect(wrapper.find('.menu-content_align-left').exists()).toEqual(true);
});
});

describe('align prop validator', () => {
it.each([
// expected, value
[true, 'left'],
[true, 'right'],
[false, 'center'],
[false, 'foo'],
])(
'returns %s if the prop value is %s',
(expected, value) => {
const result = Menu.props.align.validator(value);

expect(result).toEqual(expected);
},
);
});
});
75 changes: 48 additions & 27 deletions components/src/widgets/menu/widget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
ref="menu"
class="menu"
>
<div
class="menu-trigger"
<div
class="menu-trigger"
@click.stop="toggle"
>
<slot name="trigger" />
</div>

<div class="menu-content-wrapper">
<div
<div
v-if="showMenu"
class="menu-content"
:class="alignmentClass"
@click.stop
>
<slot name="content" />
Expand All @@ -23,39 +24,59 @@
</template>

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, computed } from 'vue'

const showMenu = ref(false)
const menu = ref(null)
const props = defineProps({
align: {
type: String,
default: 'left',
validator(value) {
return ['left', 'right'].includes(value);
},
},
});

const toggle = () => {
showMenu.value = !showMenu.value;
}
const showMenu = ref(false);
const menu = ref(null);

const alignmentClass = computed(() => (props.align === 'left'
? 'menu-content_align-left'
: 'menu-content_align-right'
));

const handleClickOutside = (event) => {
if (menu.value && !menu.value.contains(event.target)) {
showMenu.value = false;
}
const toggle = () => {
showMenu.value = !showMenu.value;
};

const handleClickOutside = (event) => {
if (menu.value && !menu.value.contains(event.target)) {
showMenu.value = false;
}
};

onMounted(() => {
document.addEventListener("click", handleClickOutside)
})
onMounted(() => {
document.addEventListener("click", handleClickOutside);
});

onUnmounted(() => {
document.removeEventListener("click", handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
</script>

<style lang="stylus" scoped>
.menu-content-wrapper {
position: relative;
}

.menu-content-wrapper {
position: relative;
}
.menu-content {
position: absolute;
top: 0;

.menu-content {
position: absolute;
top: 0;
&_align-right {
right: 0;
}
&_align-left {
left: 0;
}
</style>
}
</style>