diff --git a/components/src/assets/styles/variables.styl b/components/src/assets/styles/variables.styl
index 9729f46e..9c689b39 100644
--- a/components/src/assets/styles/variables.styl
+++ b/components/src/assets/styles/variables.styl
@@ -1 +1,2 @@
-border-color = #e0e0e0;
\ No newline at end of file
+border-color = #e0e0e0;
+base-text-color = #212121;
\ No newline at end of file
diff --git a/components/src/index.js b/components/src/index.js
index b3f0b0e2..fa868baf 100644
--- a/components/src/index.js
+++ b/components/src/index.js
@@ -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';
diff --git a/components/src/stories/Menu.stories.js b/components/src/stories/Menu.stories.js
new file mode 100644
index 00000000..738baa8f
--- /dev/null
+++ b/components/src/stories/Menu.stories.js
@@ -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: `
+
+
+ `
+ }),
+};
+
+export default {
+ title: 'Components/Menu',
+ component: Menu,
+ parameters: {
+ layout: 'centered',
+ },
+};
\ No newline at end of file
diff --git a/components/src/widgets/menu/widget.spec.js b/components/src/widgets/menu/widget.spec.js
new file mode 100644
index 00000000..90f631d2
--- /dev/null
+++ b/components/src/widgets/menu/widget.spec.js
@@ -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);
+
+ 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();
+ });
+ })
+});
diff --git a/components/src/widgets/menu/widget.vue b/components/src/widgets/menu/widget.vue
new file mode 100644
index 00000000..0b17ca21
--- /dev/null
+++ b/components/src/widgets/menu/widget.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/src/widgets/textfield/widget.vue b/components/src/widgets/textfield/widget.vue
index 0414c7cd..15245af4 100644
--- a/components/src/widgets/textfield/widget.vue
+++ b/components/src/widgets/textfield/widget.vue
@@ -61,6 +61,8 @@ export default {