Skip to content

Commit 504ddd0

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

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 fullWidth>
10+
<div
11+
slot="trigger"
12+
class="select-input__selected"
13+
>
14+
<slot name="selected">
15+
<span v-if="model">{{ selectedOption[props.propText] }}</span>
16+
<span
17+
v-else
18+
class="select-input__no-selection"
19+
>
20+
21+
</span>
22+
</slot>
23+
<ui-icon
24+
iconName="googleArrowDropDownBaseline"
25+
color="#666666"
26+
size="24"
27+
/>
28+
</div>
29+
<div
30+
slot="content"
31+
class="select-input__menu"
32+
>
33+
<div
34+
v-for="option in computedOptions"
35+
:key="option[propValue]"
36+
class="select-input__option"
37+
:class="{ 'select-input__option_selected': option[propValue] === model }"
38+
@click="setSelected(option)"
39+
>
40+
<span>{{ option[propText] }}</span>
41+
</div>
42+
</div>
43+
</ui-menu>
44+
<div
45+
v-if="hint"
46+
class="select-input__hint"
47+
>
48+
<p>{{ hint }}</p>
49+
</div>
50+
</div>
51+
</template>
52+
53+
<script setup>
54+
import { computed } from 'vue';
55+
import Menu from '~widgets/menu/widget.vue';
56+
import Icon from '~widgets/icon/widget.vue';
57+
import registerWidget from '~core/registerWidget';
58+
59+
registerWidget('ui-menu', Menu);
60+
registerWidget('ui-icon', Icon);
61+
62+
const model = defineModel({
63+
type: String,
64+
required: true,
65+
});
66+
67+
const props = defineProps({
68+
options: {
69+
type: Array,
70+
required: true,
71+
},
72+
propValue: {
73+
type: String,
74+
default: 'id',
75+
},
76+
propText: {
77+
type: String,
78+
default: 'id',
79+
},
80+
hint: {
81+
type: String,
82+
default: '',
83+
},
84+
label: {
85+
type: String,
86+
default: '',
87+
},
88+
});
89+
90+
const emit = defineEmits(['valueChange']);
91+
92+
const computedOptions = computed(() =>
93+
props.options.map((option) => {
94+
if (option && typeof option === 'object') return option;
95+
return { id: option };
96+
}),
97+
);
98+
99+
const setSelected = (option) => {
100+
const value = option[props.propValue];
101+
model.value = value;
102+
emit('valueChange', value);
103+
};
104+
105+
const selectedOption = computed(() =>
106+
computedOptions.value.find((option) => option[props.propValue] === model.value),
107+
);
108+
</script>
109+
110+
<style lang="stylus" scoped>
111+
.select-input {
112+
color: #212121;
113+
114+
&__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+
127+
&__menu {
128+
position: relative;
129+
z-index: 1;
130+
border: 1px solid #d8d8d8;
131+
border-radius: 2px;
132+
background-color: #fbfbfb;
133+
box-shadow: 0 4px 20px 0 #00000040;
134+
}
135+
&__option {
136+
height: 48px;
137+
display: flex;
138+
align-items: center;
139+
padding: 4px 12px;
140+
box-sizing: border-box;
141+
cursor: pointer;
142+
143+
&_selected {
144+
color: #2c98f0;
145+
}
146+
}
147+
148+
&__hint {
149+
margin-top: 4px;
150+
151+
p {
152+
color: #707070;
153+
font-size: 12px;
154+
font-weight: 400;
155+
line-height: 1.3;
156+
margin: 0;
157+
}
158+
}
159+
160+
&__label {
161+
margin-bottom: 8px;
162+
163+
p {
164+
font-size: 14px;
165+
font-weight: 500;
166+
line-height: 1.4;
167+
margin: 0;
168+
}
169+
}
170+
171+
&__no-selection {
172+
color: #707070;
173+
}
174+
}
175+
</style>

0 commit comments

Comments
 (0)