diff --git a/components/src/index.js b/components/src/index.js index 08d3550e..baafac2a 100644 --- a/components/src/index.js +++ b/components/src/index.js @@ -15,6 +15,9 @@ 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 Textarea } from '~widgets/textarea/widget.vue'; +export { default as Alert } from '~widgets/alert/widget.vue'; +export { default as Radio } from '~widgets/radio/widget.vue'; export { default as store } from '~core/store'; export { default as bus } from '~core/eventBus'; diff --git a/components/src/stories/Alert.stories.js b/components/src/stories/Alert.stories.js new file mode 100644 index 00000000..4fe42e08 --- /dev/null +++ b/components/src/stories/Alert.stories.js @@ -0,0 +1,30 @@ +import Alert from '~widgets/alert/widget.vue'; +import registerWidget from '~core/registerWidget'; + +registerWidget('ui-alert', Alert); + +export const Component = { + render: (args) => ({ + setup() { + return { args }; + }, + template: ``, + }), + + args: { + message: 'This is an alert item', + }, +}; + +export default { + title: 'Components/Alert', + component: Alert, + parameters: { + layout: 'centered', + }, + argTypes: { + message: 'text', + icon: 'text', + type: 'text', + }, +}; diff --git a/components/src/stories/Radio.stories.js b/components/src/stories/Radio.stories.js new file mode 100644 index 00000000..a595860a --- /dev/null +++ b/components/src/stories/Radio.stories.js @@ -0,0 +1,32 @@ +import Radio from '~widgets/radio/widget.vue'; +import registerWidget from '~core/registerWidget'; + +registerWidget('ui-radio', Radio); + +export const Component = { + render: (args) => ({ + setup() { + return { args }; + }, + template: ``, + }), + + args: { + label: 'Option', + selectedValue: 'foo', + radioValue: 'foo', + }, +}; + +export default { + title: 'Components/Radio', + component: Radio, + parameters: { + layout: 'centered', + }, + argTypes: { + label: 'text', + radioValue: 'text', + selectedValue: 'text', + }, +}; diff --git a/components/src/stories/Textarea.stories.js b/components/src/stories/Textarea.stories.js new file mode 100644 index 00000000..f8aa1bb0 --- /dev/null +++ b/components/src/stories/Textarea.stories.js @@ -0,0 +1,36 @@ +import Textarea from '~widgets/textarea/widget.vue'; +import registerWidget from '~core/registerWidget'; + +registerWidget('ui-textarea', Textarea); + +export const Basic = { + name: 'Basic options', + render: (args) => ({ + setup() { + return { args }; + }, + template: '', + }), + + args: { + value: '', + placeholder: 'Placeholder text', + }, +}; + +export default { + title: 'Components/Textarea', + component: Textarea, + parameters: { + layout: 'centered', + }, + argTypes: { + value: 'text', + readonly: 'boolean', + placeholder: 'text', + required: 'boolean', + autoGrow: 'boolean', + noBorder: 'boolean', + rows: 'number', + }, +}; diff --git a/components/src/widgets/alert/widget.spec.js b/components/src/widgets/alert/widget.spec.js new file mode 100644 index 00000000..21ac8ff2 --- /dev/null +++ b/components/src/widgets/alert/widget.spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from '@vue/test-utils'; + +import Alert from './widget.vue'; + +describe('Alert component', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(Alert, { + props: { + modelValue: true, + message: 'this is a message', + type: 'success', + }, + global: { + renderStubDefaultSlot: true, + }, + }); + }); + + describe('render', () => { + test('renders the base component', () => { + expect(wrapper.get('.alert-holder').attributes()).toEqual( + expect.objectContaining({ + class: 'alert-holder alert_success', + modelvalue: 'true', + }), + ); + + const text = wrapper.find('.alert__text'); + + expect(text.text()).toEqual('this is a message'); + + const icon = wrapper.find('ui-icon'); + + expect(icon.exists()).toEqual(true); + expect(icon.attributes()).toEqual( + expect.objectContaining({ + iconname: 'googleInfoBaseline', + color: '#0bb071', + size: '24', + }), + ); + }); + }); +}); diff --git a/components/src/widgets/alert/widget.vue b/components/src/widgets/alert/widget.vue new file mode 100644 index 00000000..65a192a1 --- /dev/null +++ b/components/src/widgets/alert/widget.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/components/src/widgets/radio/widget.spec.js b/components/src/widgets/radio/widget.spec.js new file mode 100644 index 00000000..c1546465 --- /dev/null +++ b/components/src/widgets/radio/widget.spec.js @@ -0,0 +1,91 @@ +import RadioInput from './widget.vue'; +import { shallowMount } from '@vue/test-utils'; + +describe('RadioInput component', () => { + describe('render', () => { + describe('when is checked', () => { + test('renders the base component', () => { + const wrapper = shallowMount(RadioInput, { + props: { + radioValue: 'foo', + label: 'My radio input', + selectedValue: 'foo', + }, + }); + + expect(wrapper.get('.radio-input ui-icon').attributes()).toEqual( + expect.objectContaining({ + iconname: 'googleRadioButtonCheckedBaseline', + color: '#2C98F0', + }), + ); + expect(wrapper.get('.radio-input__label').classes()).not.toContain( + 'radio-input__label_empty', + ); + expect(wrapper.get('.radio-input__label-text').text()).toEqual('My radio input'); + expect(wrapper.vm.isSelected).toEqual(true); + expect(wrapper.vm.icon).toEqual('googleRadioButtonCheckedBaseline'); + expect(wrapper.vm.iconColor).toEqual('#2C98F0'); + }); + }); + + describe('when is unchecked', () => { + test('renders the base component', () => { + const wrapper = shallowMount(RadioInput, { + props: { + radioValue: 'foo', + label: 'My radio input', + selectedValue: 'bar', + }, + }); + + expect(wrapper.get('.radio-input ui-icon').attributes()).toEqual( + expect.objectContaining({ + iconname: 'googleRadioButtonUncheckedBaseline', + color: '', + }), + ); + expect(wrapper.get('.radio-input__label').classes()).not.toContain( + 'radio-input__label_empty', + ); + expect(wrapper.get('.radio-input__label-text').text()).toEqual('My radio input'); + expect(wrapper.vm.isSelected).toEqual(false); + expect(wrapper.vm.icon).toEqual('googleRadioButtonUncheckedBaseline'); + expect(wrapper.vm.iconColor).toEqual(''); + }); + }); + + describe('when there is no label', () => { + test('adds the "radio-input__label_empty" class to the label element if there is no label', () => { + const wrapper = shallowMount(RadioInput, { + props: { + radioValue: 'foo', + selectedValue: 'foo', + label: '', + }, + }); + + expect(wrapper.get('.radio-input__label').classes()).toContain('radio-input__label_empty'); + }); + }); + }); + + describe('events', () => { + describe('#select', () => { + test('it triggers the selected event with the radio value', () => { + const wrapper = shallowMount(RadioInput, { + props: { + radioValue: 'bar', + selectedValue: 'foo', + label: 'label', + }, + }); + + wrapper.vm.select(); + + expect(wrapper.emitted('selected')).toBeTruthy(); + expect(wrapper.emitted()).toEqual({ selected: [['bar']] }); + }); + }); + }); +}); diff --git a/components/src/widgets/radio/widget.vue b/components/src/widgets/radio/widget.vue new file mode 100644 index 00000000..a5b653bb --- /dev/null +++ b/components/src/widgets/radio/widget.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/components/src/widgets/textarea/widget.spec.js b/components/src/widgets/textarea/widget.spec.js new file mode 100644 index 00000000..040289d4 --- /dev/null +++ b/components/src/widgets/textarea/widget.spec.js @@ -0,0 +1,96 @@ +import { shallowMount } from '@vue/test-utils'; + +import TextArea from './widget.vue'; + +describe('TextArea component', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(TextArea, { + props: { + modelValue: 'textarea text', + noBorder: true, + autoGrow: true, + rows: 4, + placeholder: 'placeholder', + }, + global: { + renderStubDefaultSlot: true, + }, + }); + + console.log(wrapper.html()) + }); + + describe('render', () => { + test('renders the base component', () => { + expect(wrapper.get('.textarea-field').attributes()).toEqual( + expect.objectContaining({ + class: 'textarea-field textarea-field_optional', + }), + ); + + expect(wrapper.get('textarea').attributes()).toEqual( + expect.objectContaining({ + class: 'textarea-field__input textarea-field__no-resize textarea-field__no-border', + name: 'textarea', + placeholder: 'placeholder', + rows: '4', + style: 'height: 96px;', + }), + ); + }); + }); + + describe('validation', () => { + let rule1; + let rule2; + + beforeEach(async () => { + rule1 = jest.fn().mockReturnValue(true); + rule2 = jest.fn().mockReturnValue('This field is invalid'); + + wrapper = shallowMount(TextArea, { + props: { + hint: 'Hint text', + rules: [rule1, rule2], + }, + }); + + await wrapper.get('.textarea-field__input').setValue('foo'); + }); + + it('validates the input value against the rules prop', () => { + expect(rule1).toHaveBeenCalledWith('foo'); + expect(rule2).toHaveBeenCalledWith('foo'); + }); + + it('renders the error messages if validation fails', () => { + expect(wrapper.get('.textarea-field__error-message').text()).toEqual('This field is invalid.'); + }); + + it('adds the "textarea-field_invalid" class to the element', () => { + expect(wrapper.classes()).toContain('textarea-field_invalid'); + }); + }); + + describe('#calculateInputHeight', () => { + test('calculates and sets the right height when is autogrow', () => { + wrapper.vm.calculateInputHeight(); + + expect(wrapper.get('textarea').wrapperElement.style.height).toEqual('96px'); + }); + }); + + describe('#onMounted', () => { + test('calls calculateInputHeight and sets the right height', () => { + expect(wrapper.get('textarea').wrapperElement.style.height).toEqual('96px'); + }); + }); + + describe('watch', () => { + test('calls calculateInputHeight and sets the right height', () => { + expect(wrapper.get('textarea').wrapperElement.style.height).toEqual('96px'); + }); + }); +}); diff --git a/components/src/widgets/textarea/widget.vue b/components/src/widgets/textarea/widget.vue new file mode 100644 index 00000000..31326661 --- /dev/null +++ b/components/src/widgets/textarea/widget.vue @@ -0,0 +1,193 @@ + + + + +