@@ -39,6 +125,11 @@
+```
+
+## Multiselect
+
+Use the multiple prop to allow the user selecting multiple values.
+
+```
+
+
+
+ Your colors: {{ color }}
+
+
+
+```
+
+## Options as objects
+
+Options can be objects with a label and a value property.
+
+```
+
+
+
+ Your color: {{ color }}
+
+
+
+```
+
+## Validation
+
+We use
async-validator schemas for validation.
+
+If you need to validate more than one field it is better to use the form component.
+
+```
+
+
+
+
+
+
+```
+
+## Select sizes
+
+```
+
+
+
+```
+
+## Select icons
+
+Add an icon to help the user identify the select fields usage.
+
+```
+
+
+
+
```
\ No newline at end of file
diff --git a/styleguide/src/system/components/data-input/Select/spec.js b/styleguide/src/system/components/data-input/Select/spec.js
new file mode 100755
index 000000000..88f7da935
--- /dev/null
+++ b/styleguide/src/system/components/data-input/Select/spec.js
@@ -0,0 +1,309 @@
+import { shallowMount } from '@vue/test-utils'
+import Comp from './Select.vue'
+
+describe('Select.vue', () => {
+ describe('Events emitting', () => {
+ describe('@input', () => {
+ test('should be called when the value is changed passing the new value', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '3',
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.selectOption(wrapper.vm.options[0])
+ expect(wrapper.emitted().input[0]).toEqual(['1'])
+ })
+ test('should be called when an option is clicked passing the options value', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '3',
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.find('.ds-select-option').trigger('click')
+ expect(wrapper.emitted().input[0]).toEqual(['1'])
+ })
+ })
+ })
+
+ describe('innerValue', () => {
+ test('should contain a single selected value by default', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '1',
+ options: ['1', '2', '3']
+ }
+ })
+ expect(wrapper.vm.innerValue).toEqual('1')
+ })
+ test('should contain an array of values when multiple: true', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: ['1'],
+ options: ['1', '2', '3'],
+ multiple: true
+ }
+ })
+ expect(wrapper.vm.innerValue).toEqual(['1'])
+ })
+ })
+
+ describe('options', () => {
+ test('should highlight the selected value', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '1',
+ options: ['1', '2', '3']
+ }
+ })
+ const option = wrapper.find('.ds-select-option')
+ expect(option.classes()).toContain('ds-select-option-is-selected')
+ })
+ test('should highlight all selected values when multiple: true', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: ['1', '2'],
+ options: ['1', '2', '3'],
+ multiple: true
+ }
+ })
+ const option = wrapper.findAll('.ds-select-option')
+ expect(option.at(0).classes()).toContain('ds-select-option-is-selected')
+ expect(option.at(1).classes()).toContain('ds-select-option-is-selected')
+ })
+ })
+
+ describe('selectOption', () => {
+ test('should set innerValue to selected value', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '3',
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.selectOption(wrapper.vm.options[0])
+ expect(wrapper.vm.innerValue).toEqual('1')
+ })
+ test('should add selected value to innerValue when multiple: true', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: ['3'],
+ options: ['1', '2', '3'],
+ multiple: true
+ }
+ })
+ wrapper.vm.selectOption(wrapper.vm.options[0])
+ expect(wrapper.vm.innerValue).toEqual(['3', '1'])
+ })
+ test('should toggle selected value in innerValue when multiple: true', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: ['3', '1'],
+ options: ['1', '2', '3'],
+ multiple: true
+ }
+ })
+ wrapper.vm.selectOption(wrapper.vm.options[0])
+ expect(wrapper.vm.innerValue).toEqual(['3'])
+ })
+ })
+
+ describe('search', () => {
+ test('should filter options by search string', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['cat', 'duck', 'dog']
+ }
+ })
+ wrapper.vm.searchString = 'do'
+ expect(wrapper.vm.filteredOptions).toEqual(['dog'])
+ })
+
+ test('should be case insensitive', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['cat', 'duck', 'dog']
+ }
+ })
+ wrapper.vm.searchString = 'DO'
+ expect(wrapper.vm.filteredOptions).toEqual(['dog'])
+ })
+
+ test('should ignore spaces', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['cat', 'duck', 'dog']
+ }
+ })
+ wrapper.vm.searchString = 'd o'
+ expect(wrapper.vm.filteredOptions).toEqual(['dog'])
+ })
+
+ test('should display filtered options', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['cat', 'duck', 'dog']
+ }
+ })
+ wrapper.vm.searchString = 'do'
+ const filteredOptions = wrapper.findAll('.ds-select-option')
+ expect(filteredOptions.length).toEqual(1)
+ })
+
+ test('should work when using search input', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['cat', 'duck', 'dog']
+ }
+ })
+ const searchInput = wrapper.find('.ds-select-search')
+ searchInput.setValue('do')
+ expect(wrapper.vm.filteredOptions).toEqual(['dog'])
+ })
+ })
+
+ describe('pointer', () => {
+ test('should be set by mouse over option', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ const options = wrapper.findAll('.ds-select-option')
+ options.at(2).trigger('mouseover')
+ expect(wrapper.vm.pointer).toEqual(2)
+ })
+
+ test('should be set by pointerNext', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointerNext()
+ expect(wrapper.vm.pointer).toEqual(1)
+ })
+
+ test('should be set to 0 by pointerNext when on last entry', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointer = 2
+ wrapper.vm.pointerNext()
+ expect(wrapper.vm.pointer).toEqual(0)
+ })
+
+ test('should be set by pointerPrev', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointer = 1
+ wrapper.vm.pointerPrev()
+ expect(wrapper.vm.pointer).toEqual(0)
+ })
+
+ test('should be set to last entry by pointerPrev when 0', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointerPrev()
+ expect(wrapper.vm.pointer).toEqual(2)
+ })
+
+ test('should be set by key down on wrap', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ const wrap = wrapper.find('.ds-select-wrap')
+ wrap.trigger('keydown.down')
+ expect(wrapper.vm.pointer).toEqual(1)
+ })
+
+ test('should be set by key up on wrap', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ const wrap = wrapper.find('.ds-select-wrap')
+ wrap.trigger('keydown.up')
+ expect(wrapper.vm.pointer).toEqual(2)
+ })
+
+ test('should be set by key down on search input', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ const searchInput = wrapper.find('.ds-select-search')
+ searchInput.trigger('keydown.down')
+ expect(wrapper.vm.pointer).toEqual(1)
+ })
+
+ test('should be set by key up on search input', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ const searchInput = wrapper.find('.ds-select-search')
+ searchInput.trigger('keydown.up')
+ expect(wrapper.vm.pointer).toEqual(2)
+ })
+
+ test('should select option by pointer value', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointer = 1
+ wrapper.vm.selectPointerOption()
+ expect(wrapper.vm.innerValue).toEqual('2')
+ })
+
+ test('should select option by enter key on wrap', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointer = 1
+ const wrap = wrapper.find('.ds-select-wrap')
+ wrap.trigger('keypress.enter')
+ expect(wrapper.vm.innerValue).toEqual('2')
+ })
+
+ test('should select option by enter key on search input', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ options: ['1', '2', '3']
+ }
+ })
+ wrapper.vm.pointer = 1
+ const searchInput = wrapper.find('.ds-select-search')
+ searchInput.trigger('keypress.enter')
+ expect(wrapper.vm.innerValue).toEqual('2')
+ })
+ })
+
+ it('matches snapshot', () => {
+ const wrapper = shallowMount(Comp, {
+ propsData: {
+ value: '1',
+ options: ['1', '2', '3']
+ }
+ })
+ expect(wrapper.element).toMatchSnapshot()
+ })
+})
\ No newline at end of file
diff --git a/styleguide/src/system/components/data-input/Select/style.scss b/styleguide/src/system/components/data-input/Select/style.scss
old mode 100644
new mode 100755
index 5b7a84690..524df126a
--- a/styleguide/src/system/components/data-input/Select/style.scss
+++ b/styleguide/src/system/components/data-input/Select/style.scss
@@ -1,3 +1,149 @@
@import '../shared/input.scss';
-@include input(ds-select);
\ No newline at end of file
+@include input(ds-select);
+
+.ds-select {
+ user-select: none;
+ .ds-input-has-focus & {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
+
+.ds-select-search, .ds-select-value {
+ box-sizing: border-box;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border: $input-border-size solid transparent;
+ padding: $input-padding-vertical $space-x-small;
+ line-height: $line-height-base;
+
+ .ds-input-size-small & {
+ padding: $input-padding-vertical-small $space-x-small;
+ }
+
+ .ds-input-size-large & {
+ padding: $input-padding-vertical-large $space-x-small;
+ }
+
+ .ds-select-has-icon & {
+ padding-left: $input-height;
+
+ .ds-input-size-small & {
+ padding-left: $input-height-small;
+ }
+
+ .ds-input-size-large & {
+ padding-left: $input-height-large;
+ }
+ }
+
+ .ds-select-has-icon-right & {
+ padding-right: $input-height;
+
+ .ds-input-size-small & {
+ padding-right: $input-height-small;
+ }
+
+ .ds-input-size-large & {
+ padding-right: $input-height-large;
+ }
+ }
+}
+
+.ds-select-search {
+ appearance: none;
+ font-size: inherit;
+ font-family: $font-family-text;
+ width: 100%;
+ background: transparent;
+ color: $text-color-base;
+ outline: none;
+ user-select: text;
+
+ opacity: 0;
+
+ &::placeholder {
+ color: $text-color-disabled;
+ }
+
+ .ds-input-has-focus & {
+ opacity: 1;
+ }
+
+ .ds-select-multiple & {
+ position: relative;
+ display: inline-flex;
+ width: auto;
+ height: auto;
+ padding: 0;
+ opacity: 1;
+ }
+}
+
+.ds-select-placeholder, .ds-select-value {
+ pointer-events: none;
+
+ .ds-input-has-focus & {
+ opacity: 0;
+ }
+}
+
+.ds-select-placeholder {
+ color: $text-color-disabled;
+}
+
+.ds-selected-options {
+ display: flex;
+}
+
+.ds-selected-option {
+ display: inline-flex;
+ margin-right: $space-xx-small;
+}
+
+.ds-select-dropdown {
+ position: absolute;
+ z-index: $z-index-dropdown;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ background-color: $background-color-base;
+ border: $input-border-size solid $border-color-active;
+ border-top: 0;
+ border-bottom-left-radius: $border-radius-base;
+ border-bottom-right-radius: $border-radius-base;
+ visibility: hidden;
+ opacity: 0;
+ transition: all $duration-short $ease-out;
+ max-height: 240px;
+ overflow: auto;
+
+ .ds-input-has-focus & {
+ visibility: visible;
+ opacity: 1;
+ }
+}
+
+.ds-select-options {
+ @include reset-list;
+}
+
+.ds-select-option {
+ padding: $input-padding-vertical $space-x-small;
+ cursor: pointer;
+ transition: all $duration-short $ease-out;
+
+ &.ds-select-option-hover {
+ background-color: $background-color-primary;
+ color: $text-color-primary-inverse;
+ }
+}
+
+.ds-select-option-is-selected {
+ background-color: $background-color-soft;
+ color: $text-color-primary;
+}
diff --git a/styleguide/src/system/components/data-input/shared/input.js b/styleguide/src/system/components/data-input/shared/input.js
old mode 100644
new mode 100755
index 2780a3dff..e93f96ec5
--- a/styleguide/src/system/components/data-input/shared/input.js
+++ b/styleguide/src/system/components/data-input/shared/input.js
@@ -20,7 +20,7 @@ export default {
* The value of the input. Can be passed via v-model.
*/
value: {
- type: [String, Object, Number],
+ type: [String, Object, Number, Array],
default: null
},
/**
@@ -56,7 +56,7 @@ export default {
*/
schema: {
type: Object,
- default: () => ({})
+ default: () => null
},
/**
* The input's size.
@@ -68,6 +68,10 @@ export default {
validator: value => {
return value.match(/(small|base|large)/)
}
+ },
+ tabindex: {
+ type: Number,
+ default: 0
}
},
data() {
@@ -107,9 +111,13 @@ export default {
}
},
methods: {
- input(event) {
+ handleInput(event) {
+ this.input(event.target.value)
+ },
+ input(value) {
+ this.innerValue = value
if (this.$parentForm) {
- this.$parentForm.update(this.model, event.target.value)
+ this.$parentForm.update(this.model, value)
} else {
/**
* Fires after user input.
@@ -117,8 +125,8 @@ export default {
*
* @event input
*/
- this.$emit('input', event.target.value)
- this.validate(event.target.value)
+ this.$emit('input', value)
+ this.validate(value)
}
},
handleFormUpdate(data, errors) {
@@ -126,6 +134,9 @@ export default {
this.error = errors ? errors[this.model] : null
},
validate(value) {
+ if (!this.schema) {
+ return
+ }
const validator = new Schema({ input: this.schema })
// Prevent validator from printing to console
// eslint-disable-next-line
diff --git a/styleguide/src/system/components/data-input/shared/input.scss b/styleguide/src/system/components/data-input/shared/input.scss
old mode 100644
new mode 100755
index 5991feb99..b0f30f5db
--- a/styleguide/src/system/components/data-input/shared/input.scss
+++ b/styleguide/src/system/components/data-input/shared/input.scss
@@ -2,63 +2,67 @@
.#{$class}-wrap {
position: relative;
}
-
+
.#{$class} {
+ appearance: none;
box-sizing: border-box;
- font-size: $font-size-base;
+ font-size: $input-font-size-base;
line-height: $line-height-base;
font-family: $font-family-text;
width: 100%;
padding: $input-padding-vertical $space-x-small;
height: $input-height;
-
+
color: $text-color-base;
background: $background-color-base;
-
- border: $input-border-size solid $border-color-soft;
+
+ border: $input-border-size solid $border-color-base;
border-radius: $border-radius-base;
outline: none;
transition: all $duration-short $ease-out;
-
+
&::placeholder {
color: $text-color-disabled;
}
-
+
+ .ds-input-has-focus &,
&:focus {
border-color: $border-color-active;
background: $background-color-base;
}
-
- .#{$class}-is-disabled &,
+
+ .ds-input-is-disabled &,
&:disabled {
color: $text-color-disabled;
opacity: $opacity-disabled;
cursor: not-allowed;
}
-
- .#{$class}-has-error & {
+
+ .ds-input-has-error & {
border-color: $border-color-danger;
}
}
-
- .#{$class}-size-small {
+
+ .ds-input-size-small {
font-size: $font-size-small;
-
+
.#{$class} {
+ font-size: $input-font-size-small;
height: $input-height-small;
padding: $input-padding-vertical-small $space-x-small;
}
}
-
- .#{$class}-size-large {
+
+ .ds-input-size-large {
font-size: $font-size-large;
-
+
.#{$class} {
+ font-size: $input-font-size-large;
height: $input-height-large;
padding: $input-padding-vertical-large $space-x-small;
}
}
-
+
.#{$class}-icon,
.#{$class}-icon-right {
position: absolute;
@@ -71,32 +75,47 @@
width: $input-height;
color: $text-color-softer;
transition: color $duration-short $ease-out;
-
- .#{$class}-has-focus & {
+ pointer-events: none;
+
+ .ds-input-has-focus & {
color: $text-color-base;
}
- }
+ .ds-input-size-small & {
+ width: $input-height-small;
+ }
+
+ .ds-input-size-large & {
+ width: $input-height-large;
+ }
+ }
+
.#{$class}-icon-right {
right: 0;
left: auto;
}
-
+
.#{$class}-has-icon {
padding-left: $input-height;
+
+ .ds-input-size-small & {
+ padding-left: $input-height-small;
+ }
- .#{$class}-size-small &,
- .#{$class}-size-large & {
- padding-left: $input-height;
+ .ds-input-size-large & {
+ padding-left: $input-height-large;
}
}
-
+
.#{$class}-has-icon-right {
padding-right: $input-height;
- .#{$class}-size-small &,
- .#{$class}-size-large & {
- padding-right: $input-height;
+ .ds-input-size-small & {
+ padding-right: $input-height-small;
}
- }
+
+ .ds-input-size-large & {
+ padding-right: $input-height-large;
+ }
+ }
}
diff --git a/styleguide/src/system/components/data-input/shared/multiinput.js b/styleguide/src/system/components/data-input/shared/multiinput.js
new file mode 100755
index 000000000..8666b9812
--- /dev/null
+++ b/styleguide/src/system/components/data-input/shared/multiinput.js
@@ -0,0 +1,49 @@
+/**
+ * @mixin
+ */
+export default {
+ props: {
+ /**
+ * Whether the user can select multiple items
+ */
+ multiple: {
+ type: Boolean,
+ default: false
+ }
+ },
+ methods: {
+ selectOption(option) {
+ const newValue = option.value || option
+ if (this.multiple) {
+ this.selectMultiOption(newValue)
+ } else {
+ this.input(newValue)
+ }
+ },
+ selectMultiOption(value) {
+ if (!this.innerValue) {
+ return this.input([value])
+ }
+ const index = this.innerValue.indexOf(value)
+ if (index < 0) {
+ return this.input([...this.innerValue, value])
+ }
+ this.deselectOption(index)
+ },
+ deselectOption(index) {
+ const newArray = [...this.innerValue]
+ newArray.splice(index, 1)
+ this.input(newArray)
+ },
+ isSelected(option) {
+ if (!this.innerValue) {
+ return false
+ }
+ const value = option.value || option
+ if (this.multiple) {
+ return this.innerValue.includes(value)
+ }
+ return this.innerValue === value
+ }
+ }
+}
diff --git a/styleguide/src/system/components/layout/Card/Card.vue b/styleguide/src/system/components/layout/Card/Card.vue
index d45f047cd..c55e1e81b 100644
--- a/styleguide/src/system/components/layout/Card/Card.vue
+++ b/styleguide/src/system/components/layout/Card/Card.vue
@@ -14,9 +14,9 @@
v-if="image || $slots.image">
-