Skip to content

Commit 689c882

Browse files
committed
LITE-29837: Add simple Select component
- Port simple select component from the Connect BI Reporter extension
1 parent d210121 commit 689c882

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

components/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { default as Icon } from '~widgets/icon/widget.vue';
99
export { default as View } from '~widgets/view/widget.vue';
1010
export { default as Navigation } from '~widgets/navigation/widget.vue';
1111
export { default as Status } from '~widgets/status/widget.vue';
12+
export { default as Select } from '~widgets/select/widget.vue';
1213
export { default as Textfield } from '~widgets/textfield/widget.vue';
1314
export { default as Table } from '~widgets/table/widget.vue';
1415
export { default as ComplexTable } from './widgets/complexTable/widget.vue';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { ref } from 'vue';
2+
3+
import Select from '~widgets/select/widget.vue';
4+
import registerWidget from '~core/registerWidget';
5+
6+
registerWidget('ui-select', Select);
7+
8+
export const Basic = {
9+
name: 'Basic options',
10+
render: (args) => ({
11+
setup() {
12+
return { args };
13+
},
14+
template: '<ui-select v-bind="args" style="width:200px;"></ui-select>',
15+
}),
16+
17+
args: {
18+
label: 'Label text',
19+
modelValue: '',
20+
hint: 'Some hint text',
21+
options: ['foo', 'bar', 'baz'],
22+
},
23+
};
24+
25+
export const Object = {
26+
name: 'Array of objects in options',
27+
render: Basic.render,
28+
args: {
29+
...Basic.args,
30+
propValue: 'id',
31+
propText: 'name',
32+
options: [
33+
{ id: 'OBJ-123', name: 'The first object' },
34+
{ id: 'OBJ-456', name: 'The second object' },
35+
{ id: 'OBJ-789', name: 'The third object' },
36+
],
37+
},
38+
};
39+
40+
export const Events = {
41+
name: 'Using v-model',
42+
render: (args) => ({
43+
setup() {
44+
const selectedItem = ref('');
45+
const setSelectedItem = (event) => {
46+
selectedItem.value = event.detail[0];
47+
};
48+
49+
return { args, selectedItem, setSelectedItem };
50+
},
51+
template: `
52+
<div>
53+
<p>The current selected value is: {{ selectedItem }}</p>
54+
<ui-select
55+
v-bind="args"
56+
:modelValue="selectedItem"
57+
@update:modelValue="setSelectedItem"
58+
style="width:200px;"
59+
/>
60+
</div>
61+
`,
62+
}),
63+
args: Basic.args,
64+
};
65+
66+
export default {
67+
title: 'Components/Select',
68+
component: Select,
69+
parameters: {
70+
layout: 'centered',
71+
},
72+
argTypes: {
73+
label: 'text',
74+
modelValue: 'text',
75+
hint: 'text',
76+
propValue: 'text',
77+
propText: 'text',
78+
options: { control: 'array' },
79+
},
80+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { mount } from '@vue/test-utils';
2+
import Select from './widget.vue';
3+
4+
describe('Select', () => {
5+
let wrapper;
6+
7+
beforeEach(() => {
8+
wrapper = mount(Select, {
9+
props: {
10+
modelValue: '',
11+
options: ['foo', 'bar', 'baz'],
12+
label: 'My select',
13+
hint: 'Some random hint',
14+
},
15+
});
16+
});
17+
18+
describe('render', () => {
19+
it('renders the base component', () => {
20+
expect(wrapper.get('.select-input__label').text()).toEqual('My select');
21+
expect(wrapper.get('.select-input__hint').text()).toEqual('Some random hint');
22+
expect(wrapper.get('.select-input__no-selection').text()).toEqual('—');
23+
});
24+
25+
it('renders a simple array of text elements', () => {
26+
const menuOptions = wrapper.findAll('.select-input__option');
27+
28+
expect(menuOptions.length).toEqual(3);
29+
expect(menuOptions[0].text()).toEqual('foo');
30+
expect(menuOptions[1].text()).toEqual('bar');
31+
expect(menuOptions[2].text()).toEqual('baz');
32+
});
33+
34+
it('renders a complex array of objects', async () => {
35+
await wrapper.setProps({
36+
options: [
37+
{ id: '123', external_id: 'ext-123', name: 'Foo' },
38+
{ id: '456', external_id: 'ext-456', name: 'Bar' },
39+
{ id: '789', external_id: 'ext-789', name: 'Baz' },
40+
],
41+
propValue: 'external_id',
42+
propText: 'name',
43+
});
44+
45+
const menuOptions = wrapper.findAll('.select-input__option');
46+
47+
expect(menuOptions.length).toEqual(3);
48+
expect(menuOptions[0].text()).toEqual('Foo');
49+
expect(menuOptions[1].text()).toEqual('Bar');
50+
expect(menuOptions[2].text()).toEqual('Baz');
51+
});
52+
});
53+
54+
describe('events', () => {
55+
describe('when an item is selected', () => {
56+
beforeEach(async () => {
57+
await wrapper.findAll('.select-input__option')[1].trigger('click');
58+
});
59+
60+
it('renders the selected item', () => {
61+
expect(wrapper.get('.select-input__option_selected').text()).toEqual('bar');
62+
expect(wrapper.get('.select-input__selected').text()).toEqual('bar');
63+
});
64+
65+
it('emits the update:modelValue event with the selected value', () => {
66+
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['bar']);
67+
});
68+
69+
it('emits the valueChange event with the selected value', () => {
70+
expect(wrapper.emitted('valueChange')[0]).toEqual(['bar']);
71+
});
72+
});
73+
});
74+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<template>
2+
<div class="select-input">
3+
<div
4+
v-if="label"
5+
class="select-input__label"
6+
>
7+
<p>{{ label }}</p>
8+
</div>
9+
<ui-menu
10+
fullWidth
11+
closeOnClickInside
12+
>
13+
<div
14+
slot="trigger"
15+
class="select-input__selected"
16+
>
17+
<slot name="selected">
18+
<span v-if="model">{{ selectedOption[props.propText] }}</span>
19+
<span
20+
v-else
21+
class="select-input__no-selection"
22+
>
23+
24+
</span>
25+
</slot>
26+
<ui-icon
27+
iconName="googleArrowDropDownBaseline"
28+
color="#666666"
29+
size="24"
30+
/>
31+
</div>
32+
<div
33+
slot="content"
34+
class="select-input__menu"
35+
>
36+
<div
37+
v-for="option in computedOptions"
38+
:key="option[propValue]"
39+
class="select-input__option"
40+
:class="{ 'select-input__option_selected': option[propValue] === model }"
41+
@click="setSelected(option)"
42+
>
43+
<span>{{ option[propText] }}</span>
44+
</div>
45+
</div>
46+
</ui-menu>
47+
<div
48+
v-if="hint"
49+
class="select-input__hint"
50+
>
51+
<p>{{ hint }}</p>
52+
</div>
53+
</div>
54+
</template>
55+
56+
<script setup>
57+
import { computed } from 'vue';
58+
import Menu from '~widgets/menu/widget.vue';
59+
import Icon from '~widgets/icon/widget.vue';
60+
import registerWidget from '~core/registerWidget';
61+
62+
registerWidget('ui-menu', Menu);
63+
registerWidget('ui-icon', Icon);
64+
65+
const model = defineModel({
66+
type: String,
67+
required: true,
68+
});
69+
70+
const props = defineProps({
71+
options: {
72+
type: Array,
73+
required: true,
74+
},
75+
propValue: {
76+
type: String,
77+
default: 'id',
78+
},
79+
propText: {
80+
type: String,
81+
default: 'id',
82+
},
83+
hint: {
84+
type: String,
85+
default: '',
86+
},
87+
label: {
88+
type: String,
89+
default: '',
90+
},
91+
});
92+
93+
const emit = defineEmits(['valueChange']);
94+
95+
const computedOptions = computed(() =>
96+
props.options.map((option) => {
97+
if (option && typeof option === 'object') return option;
98+
return { id: option };
99+
}),
100+
);
101+
102+
const setSelected = (option) => {
103+
const value = option[props.propValue];
104+
model.value = value;
105+
emit('valueChange', value);
106+
};
107+
108+
const selectedOption = computed(() =>
109+
computedOptions.value.find((option) => option[props.propValue] === model.value),
110+
);
111+
</script>
112+
113+
<style scoped>
114+
.select-input__selected {
115+
height: 44px;
116+
border-radius: 2px;
117+
border: 1px solid #d8d8d8;
118+
background-color: #fbfbfb;
119+
display: flex;
120+
padding: 4px 12px;
121+
align-items: center;
122+
justify-content: space-between;
123+
box-sizing: border-box;
124+
cursor: pointer;
125+
}
126+
.select-input__menu {
127+
position: relative;
128+
z-index: 1;
129+
border: 1px solid #d8d8d8;
130+
border-radius: 2px;
131+
background-color: #fbfbfb;
132+
box-shadow: 0 4px 20px 0 #00000040;
133+
}
134+
.select-input__option {
135+
height: 48px;
136+
display: flex;
137+
align-items: center;
138+
padding: 4px 12px;
139+
box-sizing: border-box;
140+
cursor: pointer;
141+
}
142+
.select-input__option_selected {
143+
color: #2c98f0;
144+
}
145+
146+
.select-input__hint {
147+
margin-top: 4px;
148+
color: #707070;
149+
font-size: 12px;
150+
font-weight: 400;
151+
line-height: 1.3;
152+
}
153+
154+
.select-input__label {
155+
font-size: 14px;
156+
font-weight: 500;
157+
line-height: 1.4;
158+
margin-bottom: 8px;
159+
}
160+
161+
.select-input__no-selection {
162+
color: #707070;
163+
}
164+
</style>

0 commit comments

Comments
 (0)