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
1 change: 1 addition & 0 deletions components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { default as Icon } from '~widgets/icon/widget.vue';
export { default as View } from '~widgets/view/widget.vue';
export { default as Navigation } from '~widgets/navigation/widget.vue';
export { default as Status } from '~widgets/status/widget.vue';
export { default as Select } from '~widgets/select/widget.vue';
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';
Expand Down
80 changes: 80 additions & 0 deletions components/src/stories/Select.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ref } from 'vue';

import Select from '~widgets/select/widget.vue';
import registerWidget from '~core/registerWidget';

registerWidget('ui-select', Select);

export const Basic = {
name: 'Basic options',
render: (args) => ({
setup() {
return { args };
},
template: '<ui-select v-bind="args" style="width:200px;"></ui-select>',
}),

args: {
label: 'Label text',
modelValue: '',
hint: 'Some hint text',
options: ['foo', 'bar', 'baz'],
},
};

export const Object = {
name: 'Array of objects in options',
render: Basic.render,
args: {
...Basic.args,
propValue: 'id',
propText: 'name',
options: [
{ id: 'OBJ-123', name: 'The first object' },
{ id: 'OBJ-456', name: 'The second object' },
{ id: 'OBJ-789', name: 'The third object' },
],
},
};

export const Events = {
name: 'Using v-model',
render: (args) => ({
setup() {
const selectedItem = ref('');
const setSelectedItem = (event) => {
selectedItem.value = event.detail[0];
};

return { args, selectedItem, setSelectedItem };
},
template: `
<div>
<p>The current selected value is: {{ selectedItem }}</p>
<ui-select
v-bind="args"
:modelValue="selectedItem"
@update:modelValue="setSelectedItem"
style="width:200px;"
/>
</div>
`,
}),
args: Basic.args,
};

export default {
title: 'Components/Select',
component: Select,
parameters: {
layout: 'centered',
},
argTypes: {
label: 'text',
modelValue: 'text',
hint: 'text',
propValue: 'text',
propText: 'text',
options: { control: 'array' },
},
};
74 changes: 74 additions & 0 deletions components/src/widgets/select/widget.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { mount } from '@vue/test-utils';
import Select from './widget.vue';

describe('Select', () => {
let wrapper;

beforeEach(() => {
wrapper = mount(Select, {
props: {
modelValue: '',
options: ['foo', 'bar', 'baz'],
label: 'My select',
hint: 'Some random hint',
},
});
});

describe('render', () => {
it('renders the base component', () => {
expect(wrapper.get('.select-input__label').text()).toEqual('My select');
expect(wrapper.get('.select-input__hint').text()).toEqual('Some random hint');
expect(wrapper.get('.select-input__no-selection').text()).toEqual('—');
});

it('renders a simple array of text elements', () => {
const menuOptions = wrapper.findAll('.select-input__option');

expect(menuOptions.length).toEqual(3);
expect(menuOptions[0].text()).toEqual('foo');
expect(menuOptions[1].text()).toEqual('bar');
expect(menuOptions[2].text()).toEqual('baz');
});

it('renders a complex array of objects', async () => {
await wrapper.setProps({
options: [
{ id: '123', external_id: 'ext-123', name: 'Foo' },
{ id: '456', external_id: 'ext-456', name: 'Bar' },
{ id: '789', external_id: 'ext-789', name: 'Baz' },
],
propValue: 'external_id',
propText: 'name',
});

const menuOptions = wrapper.findAll('.select-input__option');

expect(menuOptions.length).toEqual(3);
expect(menuOptions[0].text()).toEqual('Foo');
expect(menuOptions[1].text()).toEqual('Bar');
expect(menuOptions[2].text()).toEqual('Baz');
});
});

describe('events', () => {
describe('when an item is selected', () => {
beforeEach(async () => {
await wrapper.findAll('.select-input__option')[1].trigger('click');
});

it('renders the selected item', () => {
expect(wrapper.get('.select-input__option_selected').text()).toEqual('bar');
expect(wrapper.get('.select-input__selected').text()).toEqual('bar');
});

it('emits the update:modelValue event with the selected value', () => {
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['bar']);
});

it('emits the valueChange event with the selected value', () => {
expect(wrapper.emitted('valueChange')[0]).toEqual(['bar']);
});
});
});
});
175 changes: 175 additions & 0 deletions components/src/widgets/select/widget.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<template>
<div class="select-input">
<div
v-if="label"
class="select-input__label"
>
<p>{{ label }}</p>
</div>
<ui-menu fullWidth>
<div
slot="trigger"
class="select-input__selected"
>
<slot name="selected">
<span v-if="model">{{ selectedOption[props.propText] }}</span>
<span
v-else
class="select-input__no-selection"
>
</span>
</slot>
<ui-icon
iconName="googleArrowDropDownBaseline"
color="#666666"
size="24"
/>
</div>
<div
slot="content"
class="select-input__menu"
>
<div
v-for="option in computedOptions"
:key="option[propValue]"
class="select-input__option"
:class="{ 'select-input__option_selected': option[propValue] === model }"
@click="setSelected(option)"
>
<span>{{ option[propText] }}</span>
</div>
</div>
</ui-menu>
<div
v-if="hint"
class="select-input__hint"
>
<p>{{ hint }}</p>
</div>
</div>
</template>

<script setup>
import { computed } from 'vue';
import Menu from '~widgets/menu/widget.vue';
import Icon from '~widgets/icon/widget.vue';
import registerWidget from '~core/registerWidget';

registerWidget('ui-menu', Menu);
registerWidget('ui-icon', Icon);

const model = defineModel({
type: String,
required: true,
});

const props = defineProps({
options: {
type: Array,
required: true,
},
propValue: {
type: String,
default: 'id',
},
propText: {
type: String,
default: 'id',
},
hint: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
});

const emit = defineEmits(['valueChange']);

const computedOptions = computed(() =>
props.options.map((option) => {
if (option && typeof option === 'object') return option;
return { id: option };
}),
);

const setSelected = (option) => {
const value = option[props.propValue];
model.value = value;
emit('valueChange', value);
};

const selectedOption = computed(() =>
computedOptions.value.find((option) => option[props.propValue] === model.value),
);
</script>

<style lang="stylus" scoped>
.select-input {
color: #212121;

&__selected {
height: 44px;
border-radius: 2px;
border: 1px solid #d8d8d8;
background-color: #fbfbfb;
display: flex;
padding: 4px 12px;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: pointer;
}

&__menu {
position: relative;
z-index: 1;
border: 1px solid #d8d8d8;
border-radius: 2px;
background-color: #fbfbfb;
box-shadow: 0 4px 20px 0 #00000040;
}
&__option {
height: 48px;
display: flex;
align-items: center;
padding: 4px 12px;
box-sizing: border-box;
cursor: pointer;

&_selected {
color: #2c98f0;
}
}

&__hint {
margin-top: 4px;

p {
color: #707070;
font-size: 12px;
font-weight: 400;
line-height: 1.3;
margin: 0;
}
}

&__label {
margin-bottom: 8px;

p {
font-size: 14px;
font-weight: 500;
line-height: 1.4;
margin: 0;
}
}

&__no-selection {
color: #707070;
}
}
</style>