Skip to content
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

Feature/lite 29476 create menu component #48

Merged
merged 2 commits into from
Feb 19, 2024
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
3 changes: 2 additions & 1 deletion components/src/assets/styles/variables.styl
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
border-color = #e0e0e0;
border-color = #e0e0e0;
base-text-color = #212121;
1 change: 1 addition & 0 deletions components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as Textfield } from '~widgets/textfield/widget.vue';
export { default as Table } from '~widgets/table/widget.vue';
export { default as ComplexTable } from './widgets/complexTable/widget.vue';
export { default as Button } from '~widgets/button/widget.vue';
export { default as Menu } from '~widgets/menu/widget.vue';

export { default as store } from '~core/store';
export { default as bus } from '~core/eventBus';
Expand Down
31 changes: 31 additions & 0 deletions components/src/stories/Menu.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Menu from '~widgets/menu/widget.vue';
import Button from '~widgets/button/widget.vue';
import registerWidget from '~core/registerWidget';

registerWidget('ui-menu', Menu);
registerWidget('ui-button', Button);

export const Component = {
render: (args) => ({
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>`
}),
};

export default {
title: 'Components/Menu',
component: Menu,
parameters: {
layout: 'centered',
},
};
72 changes: 72 additions & 0 deletions components/src/widgets/menu/widget.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { mount } from '@vue/test-utils'
import Menu from './widget';

describe('Menu component', () => {
describe('methods', () => {
describe('#toggle', () => {
it('toggles menu to true when clicking', () => {
const wrapper = mount(Menu);
wrapper.vm.showMenu = false;
wrapper.vm.toggle(wrapper.vm.showMenu);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just fyi, dom events can be triggered directly instead of calling the fn attached to the listener.
Eg: wrapper.find('.menu-trigger').trigger('click'); will trigger the event listener, calling your function. You could check then that the 'menu-content' element is rendered as a consequence instead of checking the variable's value. Eg: expect(wrapper.find('.menu-content').exists()).toBeTruthy()

No need to change, just a different type of test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I will leave it as it is and try to use this way when appropriate in the future :)


expect(wrapper.vm.showMenu).toBe(true);
});

it('toggles menu back to false when clicking', () => {
const wrapper = mount(Menu);
wrapper.vm.showMenu = true;
wrapper.vm.toggle(wrapper.vm.showMenu);

expect(wrapper.vm.showMenu).toBe(false);
});
});

describe('#handleClickOutside', () => {

it('hides menu content when clicked outside menu', () => {
const event = { target: 'another value'};
const wrapper = mount(Menu);
wrapper.vm.menu = { contains: jest.fn().mockReturnValue(false) };
wrapper.vm.showMenu = true;
wrapper.vm.handleClickOutside(event);

expect(wrapper.vm.showMenu).toBe(false);
});

it('does not hide menu content when clicked inside menu', () => {
const event = { target: 'some value'};
const wrapper = mount(Menu);
wrapper.vm.menu = { contains: jest.fn().mockReturnValue(true) };
wrapper.vm.showMenu = true;
wrapper.vm.handleClickOutside(event);

expect(wrapper.vm.showMenu).toBe(true);
});
});
});

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();
});
})
});
61 changes: 61 additions & 0 deletions components/src/widgets/menu/widget.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<div
ref="menu"
class="menu"
>
<div
class="menu-trigger"
@click.stop="toggle"
>
<slot name="trigger" />
</div>

<div class="menu-content-wrapper">
<div
v-if="showMenu"
class="menu-content"
@click.stop
>
<slot name="content" />
</div>
</div>
</div>
</template>

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

const showMenu = ref(false)
const menu = ref(null)

const toggle = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutations are like some hooks under the hood, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just syntax for writing method in composition API (opposed to option API that we are used to see more :))

showMenu.value = !showMenu.value;
}

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

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

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

<style lang="stylus" scoped>

.menu-content-wrapper {
position: relative;
}

.menu-content {
position: absolute;
top: 0;
left: 0;
}
</style>
4 changes: 3 additions & 1 deletion components/src/widgets/textfield/widget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ export default {
</script>

<style lang="stylus">
@import '../../assets/styles/common.styl';

.text-field {
font-family: 'Roboto';
font-size: 14px;
line-height: 20px;
font-weight: 400;
display: flex;
flex-flow: column nowrap;
color: #212121;
color: base-text-color;

label {
font-weight 500;
Expand Down