Add v-select component (#262)

This commit is contained in:
Rijk van Zanten
2020-03-30 15:49:23 -04:00
committed by GitHub
parent d38cfefc05
commit 6205e6bf94
8 changed files with 398 additions and 2 deletions

38
.storybook/raw-value.vue Normal file
View File

@@ -0,0 +1,38 @@
<template functional>
<div class="raw-value">
<div class="label">{{ props.label }}</div>
<pre><slot /></pre>
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
props: {
label: {
type: String,
default: 'Value:',
},
},
});
</script>
<style scoped>
.raw-value {
margin: 20px 0;
}
.label {
margin-bottom: 4px;
font-weight: 500;
}
pre {
max-width: max-content;
padding: 0.5rem;
font-family: monospace;
background-color: #eee;
border-radius: 8px;
}
</style>

View File

@@ -56,7 +56,7 @@ export function usePopper(
{
...offset,
options: {
offset: options.value.attached ? [0, 0] : [0, 8],
offset: options.value.attached ? [0, -2] : [0, 8],
},
},
computeStyles,

View File

@@ -13,7 +13,7 @@
<div
ref="popper"
class="v-menu-popper"
:class="{ active: isActive }"
:class="{ active: isActive, attached }"
:data-placement="popperPlacement"
:style="styles"
>
@@ -276,5 +276,12 @@ export default defineComponent({
transition-timing-function: cubic-bezier(0, 0, 0.2, 1.5);
transition-duration: var(--medium);
}
&.attached {
.v-menu-content {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
}
</style>

View File

@@ -0,0 +1,4 @@
import VSelect from './v-select.vue';
export { VSelect };
export default VSelect;

View File

@@ -0,0 +1,44 @@
# Select
Renders a dropdown input.
## Usage
```html
<v-select
v-model="value"
:items="[
{
text: 'Item 1',
value: 'item-1',
},
{
text: 'Item 2',
value: 'item-2',
},
]"
/>
```
## Props
| Prop | Description | Default |
|---------------|-----------------------------------------------------|---------|
| `items`* | Items to render in the select | |
| `itemText` | What item value to use for the display text | `text` |
| `itemValue` | What item value to use for the item value | `value` |
| `value` | Currently selected item(s) | |
| `multiple` | Allow multiple items to be selected | `false` |
| `placeholder` | What placeholder to show when no items are selected | |
## Events
| Event | Description | Value |
|---------|--------------------------|-----------------------------------------|
| `input` | New value for the select | `(string | number)[] | string | number` |
## Slots
n/a
## CSS Variables
n/a

View File

@@ -0,0 +1,61 @@
import readme from './readme.md';
import { defineComponent, ref } from '@vue/composition-api';
import withPadding from '../../../.storybook/decorators/with-padding';
import { withKnobs, array, text } from '@storybook/addon-knobs';
import RawValue from '../../../.storybook/raw-value.vue';
import VSelect from './v-select.vue';
export default {
title: 'Components / Select',
parameters: {
notes: readme,
},
decorators: [withKnobs, withPadding],
};
export const basic = () =>
defineComponent({
components: { VSelect, RawValue },
props: {
items: {
default: array('Items', ['Item 1', 'Item 2', 'Item 3']),
},
placeholder: {
default: text('Placeholder', 'Enter value...'),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div>
<v-select :placeholder="placeholder" v-model="value" :items="items" />
<raw-value>{{ value }}</raw-value>
</div>
`,
});
export const multiple = () =>
defineComponent({
components: { VSelect, RawValue },
props: {
items: {
default: array('Items', ['Item 1', 'Item 2', 'Item 3']),
},
placeholder: {
default: text('Placeholder', 'Enter value...'),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div>
<v-select :placeholder="placeholder" v-model="value" :items="items" multiple />
<raw-value>{{ value }}</raw-value>
</div>
`,
});

View File

@@ -0,0 +1,134 @@
import VSelect from './v-select.vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VMenu from '@/components/v-menu/';
import VList, { VListItem, VListItemContent, VListItemTitle } from '@/components/v-list/';
import VCheckbox from '@/components/v-checkbox/';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-menu', VMenu);
localVue.component('v-list', VList);
localVue.component('v-list-item', VListItem);
localVue.component('v-list-item-content', VListItemContent);
localVue.component('v-list-item-title', VListItemTitle);
localVue.component('v-checkbox', VCheckbox);
describe('Components / Select', () => {
it('Renders', () => {
const component = shallowMount(VSelect, {
localVue,
propsData: {
items: [
{ text: 'test', value: 1 },
{ text: 'test', value: 2 },
],
},
});
expect(component.isVueInstance()).toBe(true);
});
it('Converts the items to a standardized array', async () => {
const component = shallowMount(VSelect, {
localVue,
propsData: {
items: ['Item 1', 'Item 2'],
},
});
expect((component.vm as any)._items).toEqual([
{
text: 'Item 1',
value: 'Item 1',
},
{
text: 'Item 2',
value: 'Item 2',
},
]);
component.setProps({
items: [
{
test: 'item 1',
another: 'value',
},
],
itemText: 'test',
itemValue: 'another',
});
await component.vm.$nextTick();
expect((component.vm as any)._items).toEqual([
{
text: 'item 1',
value: 'value',
},
]);
});
it('Calculates the displayValue based on the given value', async () => {
const component = shallowMount(VSelect, {
localVue,
propsData: {
items: ['Item 1', 'Item 2'],
value: 'Item 1',
},
});
expect((component.vm as any).displayValue).toBe('Item 1');
component.setProps({
items: [
{
text: 'Item 1',
value: 'item-1',
},
],
value: 'item-1',
});
await component.vm.$nextTick();
expect((component.vm as any).displayValue).toBe('Item 1');
component.setProps({
itemText: 'test',
itemValue: 'test2',
items: [
{
test: 'Item 1',
test2: 'item-1',
},
],
value: 'item-1',
});
await component.vm.$nextTick();
expect((component.vm as any).displayValue).toBe('Item 1');
component.setProps({
itemText: 'text',
itemValue: 'value',
items: [
{
text: 'Item 1',
value: 'item-1',
},
{
text: 'Item 2',
value: 'item-2',
},
],
multiple: true,
value: ['item-1', 'item-2'],
});
await component.vm.$nextTick();
expect((component.vm as any).displayValue).toBe('Item 1, Item 2');
});
});

View File

@@ -0,0 +1,108 @@
<template>
<v-menu class="v-select" attached :close-on-content-click="multiple === false">
<template #activator="{ toggle }">
<v-input readonly :value="displayValue" @click="toggle" :placeholder="placeholder">
<template #append><v-icon name="expand_more" /></template>
</v-input>
</template>
<v-list>
<v-list-item
v-for="item in _items"
:key="item.value"
:class="{
active: multiple ? (value || []).includes(item.value) : value === item.value,
}"
@click="multiple ? null : $emit('input', item.value)"
>
<v-list-item-content>
<v-list-item-title v-if="multiple === false">{{ item.text }}</v-list-item-title>
<v-checkbox
v-else
:inputValue="value || []"
:label="item.text"
:value="item.value"
@change="$emit('input', $event.length > 0 ? $event : null)"
/>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
type Item = {
text: string;
value: string | number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ItemsRaw = (string | any)[];
type InputValue = (string | number)[] | string | number;
export default defineComponent({
props: {
items: {
type: Array as PropType<ItemsRaw>,
required: true,
},
itemText: {
type: String,
default: 'text',
},
itemValue: {
type: String,
default: 'value',
},
value: {
type: [Array, String, Number] as PropType<InputValue>,
default: null,
},
multiple: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
},
setup(props) {
const _items = computed(() =>
props.items.map((item) => {
if (typeof item === 'string') {
return {
text: item,
value: item,
};
}
return {
text: item[props.itemText],
value: item[props.itemValue],
};
})
);
const displayValue = computed(() => {
if (Array.isArray(props.value)) {
return props.value
.map((value) => {
return getTextForValue(value);
})
.join(', ');
}
return getTextForValue(props.value);
});
return { _items, displayValue };
function getTextForValue(value: string | number) {
return _items.value.find((item) => item.value === value)?.['text'];
}
},
});
</script>