chevron_left
@@ -375,46 +411,64 @@ exports[`VCarousel.js should render component without delimiters 1`] = `
-
-
-
`;
-exports[`VCarousel.js should render component without left control icon and match snapshot 1`] = `
+exports[`VCarousel.js should render component without next icon and match snapshot 1`] = `
-
+
- chevron_right
+ chevron_left
@@ -425,19 +479,19 @@ exports[`VCarousel.js should render component without left control icon and matc
`;
-exports[`VCarousel.js should render component without right control icon and match snapshot 1`] = `
+exports[`VCarousel.js should render component without prev icon and match snapshot 1`] = `
-
+
- chevron_left
+ chevron_right
diff --git a/src/components/VCarousel/__snapshots__/VCarouselItem.spec.js.snap b/src/components/VCarousel/__snapshots__/VCarouselItem.spec.js.snap
index 7fa9fcfb446d..a2e8b715fce0 100644
--- a/src/components/VCarousel/__snapshots__/VCarouselItem.spec.js.snap
+++ b/src/components/VCarousel/__snapshots__/VCarouselItem.spec.js.snap
@@ -2,18 +2,30 @@
exports[`VCarouselItem.js should render component and match snapshot 1`] = `
-
`;
exports[`VCarouselItem.js should throw warning when not used inside v-carousel 1`] = `
-
`;
diff --git a/src/components/VCheckbox/VCheckbox.js b/src/components/VCheckbox/VCheckbox.js
index d7d458eeadf3..615e24814588 100644
--- a/src/components/VCheckbox/VCheckbox.js
+++ b/src/components/VCheckbox/VCheckbox.js
@@ -1,5 +1,5 @@
-require('../../stylus/components/_input-groups.styl')
-require('../../stylus/components/_selection-controls.styl')
+import '../../stylus/components/_input-groups.styl'
+import '../../stylus/components/_selection-controls.styl'
import VIcon from '../VIcon'
import { VFadeTransition } from '../transitions'
@@ -85,7 +85,7 @@ export default {
? -1
: this.internalTabIndex || this.tabindex,
role: 'checkbox',
- 'aria-checked': this.inputIndeterminate && 'mixed' || this.isActive && 'true' || 'false',
+ 'aria-checked': this.inputIndeterminate ? 'mixed' : (this.isActive ? 'true' : 'false'),
'aria-label': this.label
}
}
diff --git a/src/components/VCheckbox/VCheckbox.spec.js b/src/components/VCheckbox/VCheckbox.spec.js
index 1529301a702f..fca6b0467bdc 100644
--- a/src/components/VCheckbox/VCheckbox.spec.js
+++ b/src/components/VCheckbox/VCheckbox.spec.js
@@ -1,8 +1,7 @@
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
-import VCheckbox from '~components/VCheckbox'
+import { test } from '@util/testing'
+import VCheckbox from '@components/VCheckbox'
-test('VCheckbox.js', () => {
+test('VCheckbox.js', ({ mount }) => {
it('should return true when clicked', () => {
const wrapper = mount(VCheckbox, {
propsData: {
@@ -164,7 +163,7 @@ test('VCheckbox.js', () => {
expect(change).toBeCalled()
})
- it('should set ripple data attribute based on disabled state', () => {
+ it('should enable ripple based on disabled state', () => {
const wrapper = mount(VCheckbox, {
propsData: {
inputValue: false,
@@ -174,11 +173,30 @@ test('VCheckbox.js', () => {
const ripple = wrapper.find('.input-group--selection-controls__ripple')[0]
- expect(ripple.getAttribute('data-ripple')).toBe('true')
+ expect(ripple.element._ripple.enabled).toBe(true)
+ expect(ripple.element._ripple.centered).toBe(true)
wrapper.setProps({ disabled: true })
- expect(ripple.getAttribute('data-ripple')).toBe('false')
+ expect(ripple.element._ripple.enabled).toBe(false)
+ })
+
+ it('should set ripple centered property when enabled', () => {
+ const wrapper = mount(VCheckbox, {
+ propsData: {
+ inputValue: false,
+ disabled: true
+ }
+ })
+
+ const ripple = wrapper.find('.input-group--selection-controls__ripple')[0]
+
+ expect(ripple.element._ripple.enabled).toBe(false)
+
+ wrapper.setProps({ disabled: false })
+
+ expect(ripple.element._ripple.enabled).toBe(true)
+ expect(ripple.element._ripple.centered).toBe(true)
})
it('should not render ripple when ripple prop is false', () => {
@@ -191,10 +209,10 @@ test('VCheckbox.js', () => {
const ripple = wrapper.find('.input-group--selection-controls__ripple')
- expect(ripple.length).toBe(0)
+ expect(ripple).toHaveLength(0)
})
- it('should render ripple with data attribute when ripple prop is true', () => {
+ it('should render ripple when ripple prop is true', () => {
const wrapper = mount(VCheckbox, {
propsData: {
ripple: true
@@ -203,6 +221,7 @@ test('VCheckbox.js', () => {
const ripple = wrapper.find('.input-group--selection-controls__ripple')[0]
- expect(ripple.getAttribute('data-ripple')).toBe('true')
+ expect(ripple.element._ripple.enabled).toBe(true)
+ expect(ripple.element._ripple.centered).toBe(true)
})
})
diff --git a/src/components/VChip/VChip.js b/src/components/VChip/VChip.js
index 328cbb25e9e6..956b404a8a38 100644
--- a/src/components/VChip/VChip.js
+++ b/src/components/VChip/VChip.js
@@ -1,4 +1,4 @@
-require('../../stylus/components/_chips.styl')
+import '../../stylus/components/_chips.styl'
import VIcon from '../VIcon'
import Colorable from '../../mixins/colorable'
@@ -43,7 +43,7 @@ export default {
})
return (this.textColor || this.outline)
- ? this.addTextColorClassChecks(classes, this.textColor ? 'textColor' : 'color')
+ ? this.addTextColorClassChecks(classes, this.textColor || this.color)
: classes
}
},
diff --git a/src/components/VChip/VChip.spec.js b/src/components/VChip/VChip.spec.js
index 0e85d1de62a1..af72953e708c 100644
--- a/src/components/VChip/VChip.spec.js
+++ b/src/components/VChip/VChip.spec.js
@@ -1,8 +1,7 @@
-import VChip from '~components/VChip'
-import { mount } from 'avoriaz'
-import { test } from '~util/testing'
+import VChip from '@components/VChip'
+import { test } from '@util/testing'
-test('VChip.vue', () => {
+test('VChip.vue', ({ mount, compileToFunctions }) => {
it('should have a chip class', () => {
const wrapper = mount(VChip)
@@ -39,6 +38,16 @@ test('VChip.vue', () => {
expect(wrapper.element.classList).toContain('green--text')
})
+ it('should render a disabled chip', () => {
+ const wrapper = mount(VChip, {
+ propsData: {
+ disabled: true
+ }
+ })
+
+ expect(wrapper.element.classList).toContain('chip--disabled')
+ })
+
it('should render a colored outline chip', () => {
const wrapper = mount(VChip, {
propsData: {
diff --git a/src/components/VChip/__snapshots__/VChip.spec.js.snap b/src/components/VChip/__snapshots__/VChip.spec.js.snap
index 72b1a1faa8e2..ad6519d70422 100644
--- a/src/components/VChip/__snapshots__/VChip.spec.js.snap
+++ b/src/components/VChip/__snapshots__/VChip.spec.js.snap
@@ -8,7 +8,7 @@ exports[`VChip.vue should be removable 1`] = `
cancel
@@ -27,7 +27,7 @@ exports[`VChip.vue should be removable 2`] = `
cancel
diff --git a/src/components/VDataIterator/VDataIterator.js b/src/components/VDataIterator/VDataIterator.js
new file mode 100644
index 000000000000..3669d1be4de3
--- /dev/null
+++ b/src/components/VDataIterator/VDataIterator.js
@@ -0,0 +1,99 @@
+import '../../stylus/components/_data-iterator.styl'
+
+import DataIterable from '../../mixins/data-iterable'
+
+export default {
+ name: 'v-data-iterator',
+
+ mixins: [DataIterable],
+
+ inheritAttrs: false,
+
+ props: {
+ contentTag: {
+ type: String,
+ default: 'div'
+ },
+ contentProps: {
+ type: Object,
+ required: false
+ },
+ contentClass: {
+ type: String,
+ required: false
+ }
+ },
+
+ computed: {
+ classes () {
+ return {
+ 'data-iterator': true,
+ 'data-iterator--select-all': this.selectAll !== false,
+ 'theme--dark': this.dark,
+ 'theme--light': this.light
+ }
+ }
+ },
+
+ methods: {
+ genContent () {
+ const children = this.genItems()
+
+ const data = {
+ 'class': this.contentClass,
+ attrs: this.$attrs,
+ on: this.$listeners,
+ props: this.contentProps
+ }
+
+ return this.$createElement(this.contentTag, data, children)
+ },
+ genEmptyItems (content) {
+ return [this.$createElement('div', {
+ 'class': 'text-xs-center',
+ style: 'width: 100%'
+ }, content)]
+ },
+ genFilteredItems () {
+ if (!this.$scopedSlots.item) {
+ return null
+ }
+
+ const items = []
+ for (let index = 0, len = this.filteredItems.length; index < len; ++index) {
+ const item = this.filteredItems[index]
+ const props = this.createProps(item, index)
+ items.push(this.$scopedSlots.item(props))
+ }
+
+ return items
+ },
+ genFooter () {
+ const children = []
+
+ if (this.$slots.footer) {
+ children.push(this.$slots.footer)
+ }
+
+ if (!this.hideActions) {
+ children.push(this.genActions())
+ }
+
+ if (!children.length) return null
+ return this.$createElement('div', children)
+ }
+ },
+
+ created () {
+ this.initPagination()
+ },
+
+ render (h) {
+ return h('div', {
+ 'class': this.classes
+ }, [
+ this.genContent(),
+ this.genFooter()
+ ])
+ }
+}
diff --git a/src/components/VDataIterator/VDataIterator.spec.js b/src/components/VDataIterator/VDataIterator.spec.js
new file mode 100644
index 000000000000..8b1061b69efb
--- /dev/null
+++ b/src/components/VDataIterator/VDataIterator.spec.js
@@ -0,0 +1,149 @@
+import Vue from 'vue'
+import { test } from '@util/testing'
+import VDataIterator from './VDataIterator'
+import VBtn from '@components/VBtn'
+
+test('VDataIterator.js', ({ mount, compileToFunctions }) => {
+ function dataIteratorTestData () {
+ return {
+ propsData: {
+ pagination: {
+ descending: false,
+ sortBy: 'col1',
+ rowsPerPage: 5,
+ page: 1
+ },
+ items: [
+ { other: 1, col1: 'foo', col2: 'a', col3: 1 },
+ { other: 2, col1: null, col2: 'b', col3: 2 },
+ { other: 3, col1: undefined, col2: 'c', col3: 3 }
+ ]
+ }
+ }
+ }
+
+ it('should match a snapshot - no matching records', () => {
+ const data = dataIteratorTestData()
+ data.propsData.search = "asdf"
+ const wrapper = mount(VDataIterator, data)
+
+ expect(wrapper.html()).toMatchSnapshot()
+
+ const content = wrapper.find('.data-iterator div div')[0]
+ expect(content.element.textContent).toBe('No matching records found')
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should match a snapshot - hideActions and no footer slot', () => {
+ const data = dataIteratorTestData()
+ data.propsData.hideActions = true
+ const wrapper = mount(VDataIterator, data)
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match a snapshot - footer slot', () => {
+ const data = dataIteratorTestData()
+ data.slots = {
+ footer: [compileToFunctions('
footer ')],
+ }
+ const wrapper = mount(VDataIterator, data)
+
+ expect(wrapper.html()).toMatchSnapshot()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should match a snapshot - no data', () => {
+ const data = dataIteratorTestData()
+ data.propsData.items = []
+ const wrapper = mount(VDataIterator, data)
+
+ expect(wrapper.html()).toMatchSnapshot()
+
+ const content = wrapper.find('.data-iterator div div')[0]
+ expect(content.element.textContent).toBe('No data available')
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should match a snapshot - with data', () => {
+ const data = dataIteratorTestData()
+
+ const vm = new Vue()
+ const item = props => vm.$createElement('div', [props.item.col2])
+ const component = Vue.component('test', {
+ components: {
+ VBtn,
+ VDataIterator
+ },
+ render (h) {
+ return h('v-data-iterator', {
+ props: {
+ 'content-tag': 'span',
+ ...data.propsData
+ },
+ scopedSlots: {
+ item
+ }
+ })
+ }
+ })
+
+ const wrapper = mount(component)
+
+ expect(wrapper.html()).toMatchSnapshot()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should pass attrs, class and props to content', () => {
+ const data = dataIteratorTestData()
+
+ const vm = new Vue()
+ const item = props => vm.$createElement('div', [props.item.col2])
+ const component = Vue.component('test', {
+ components: {
+ VBtn,
+ VDataIterator
+ },
+ render (h) {
+ return h('v-data-iterator', {
+ props: {
+ 'content-tag': 'v-btn',
+ ...data.propsData,
+ 'content-props': { block: true },
+ 'content-class': 'test__class'
+ },
+ attrs: {
+ id: "testButtonId"
+ },
+ scopedSlots: {
+ item
+ }
+ })
+ }
+ })
+
+ const wrapper = mount(component)
+
+ const mainDiv = wrapper.find('.data-iterator')[0]
+ expect(mainDiv.hasAttribute('id')).toBe(false)
+
+ var button = mainDiv.find('button')[0]
+ expect(button.getAttribute('id')).toBe('testButtonId')
+ expect(button.hasClass('btn--block')).toBe(true)
+ expect(button.hasClass('test__class')).toBe(true)
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should not filter items if search is empty', async () => {
+ const data = dataIteratorTestData()
+ data.propsData.search = ' '
+ const wrapper = mount(VDataIterator, data)
+
+ expect(wrapper.instance().filteredItems).toHaveLength(data.propsData.items.length)
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+})
diff --git a/src/components/VDataIterator/__snapshots__/VDataIterator.spec.js.snap b/src/components/VDataIterator/__snapshots__/VDataIterator.spec.js.snap
new file mode 100644
index 000000000000..152b3364a4b4
--- /dev/null
+++ b/src/components/VDataIterator/__snapshots__/VDataIterator.spec.js.snap
@@ -0,0 +1,524 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDataIterator.js should match a snapshot - footer slot 1`] = `
+
+
+
+
+
+
+ footer
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDataIterator.js should match a snapshot - hideActions and no footer slot 1`] = `
+
+
+
+`;
+
+exports[`VDataIterator.js should match a snapshot - no data 1`] = `
+
+
+
+
+ No data available
+
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDataIterator.js should match a snapshot - no matching records 1`] = `
+
+
+
+
+ No matching records found
+
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDataIterator.js should match a snapshot - with data 1`] = `
+
+
+
+
+ b
+
+
+ c
+
+
+ a
+
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VDataIterator/index.js b/src/components/VDataIterator/index.js
new file mode 100644
index 000000000000..79f96bb7f31e
--- /dev/null
+++ b/src/components/VDataIterator/index.js
@@ -0,0 +1,7 @@
+import VDataIterator from './VDataIterator'
+
+VDataIterator.install = function install (Vue) {
+ Vue.component(VDataIterator.name, VDataIterator)
+}
+
+export default VDataIterator
diff --git a/src/components/VDataTable/VDataTable.js b/src/components/VDataTable/VDataTable.js
index 424be96abf92..a328e090e5de 100644
--- a/src/components/VDataTable/VDataTable.js
+++ b/src/components/VDataTable/VDataTable.js
@@ -1,14 +1,10 @@
-require('../../stylus/components/_tables.styl')
-require('../../stylus/components/_data-table.styl')
+import '../../stylus/components/_tables.styl'
+import '../../stylus/components/_data-table.styl'
+
+import DataIterable from '../../mixins/data-iterable'
-import VBtn from '../VBtn'
-import VIcon from '../VIcon'
import VProgressLinear from '../VProgressLinear'
-import VSelect from '../VSelect'
-import Filterable from '../../mixins/filterable'
-import Themeable from '../../mixins/themeable'
-import Loadable from '../../mixins/loadable'
import Head from './mixins/head'
import Body from './mixins/body'
import Foot from './mixins/foot'
@@ -23,35 +19,23 @@ export default {
name: 'v-data-table',
components: {
- VBtn,
- VIcon,
VProgressLinear,
- VSelect,
// Importing does not work properly
'v-table-overflow': createSimpleFunctional('table__overflow')
},
data () {
return {
- all: false,
- searchLength: 0,
- defaultPagination: {
- descending: false,
- page: 1,
- rowsPerPage: 5,
- sortBy: null,
- totalItems: 0
- },
- expanded: {}
+ actionsClasses: 'datatable__actions',
+ actionsRangeControlsClasses: 'datatable__actions__range-controls',
+ actionsSelectClasses: 'datatable__actions__select',
+ actionsPaginationClasses: 'datatable__actions__pagination'
}
},
- mixins: [Head, Body, Filterable, Foot, Loadable, Progress, Themeable],
+ mixins: [DataIterable, Head, Body, Foot, Progress],
props: {
- expand: {
- type: Boolean
- },
headers: {
type: Array,
default: () => []
@@ -60,41 +44,11 @@ export default {
type: String,
default: 'text'
},
- hideActions: Boolean,
hideHeaders: Boolean,
- disableInitialSort: Boolean,
- mustSort: Boolean,
- noResultsText: {
- type: String,
- default: 'No matching records found'
- },
- rowsPerPageItems: {
- type: Array,
- default () {
- return [
- 5,
- 10,
- 25,
- { text: 'All', value: -1 }
- ]
- }
- },
rowsPerPageText: {
type: String,
default: 'Rows per page:'
},
- selectAll: [Boolean, String],
- search: {
- required: false
- },
- filter: {
- type: Function,
- default: (val, search) => {
- return val !== null &&
- ['undefined', 'boolean'].indexOf(typeof val) === -1 &&
- val.toString().toLowerCase().indexOf(search) !== -1
- }
- },
customFilter: {
type: Function,
default: (items, search, filter, headers) => {
@@ -105,62 +59,6 @@ export default {
return items.filter(item => props.some(prop => filter(getObjectValueByPath(item, prop), search)))
}
- },
- customSort: {
- type: Function,
- default: (items, index, isDescending) => {
- if (index === null) return items
-
- return items.sort((a, b) => {
- let sortA = getObjectValueByPath(a, index)
- let sortB = getObjectValueByPath(b, index)
-
- if (isDescending) {
- [sortA, sortB] = [sortB, sortA]
- }
-
- // Check if both are numbers
- if (!isNaN(sortA) && !isNaN(sortB)) {
- return sortA - sortB
- }
-
- // Check if both cannot be evaluated
- if (sortA === null && sortB === null) {
- return 0
- }
-
- [sortA, sortB] = [sortA, sortB]
- .map(s => (
- (s || '').toString().toLocaleLowerCase()
- ))
-
- if (sortA > sortB) return 1
- if (sortA < sortB) return -1
-
- return 0
- })
- }
- },
- value: {
- type: Array,
- default: () => []
- },
- items: {
- type: Array,
- required: true,
- default: () => []
- },
- totalItems: {
- type: Number,
- default: null
- },
- itemKey: {
- type: String,
- default: 'id'
- },
- pagination: {
- type: Object,
- default: () => {}
}
},
@@ -173,142 +71,20 @@ export default {
'theme--light': this.light
}
},
- computedPagination () {
- return this.hasPagination
- ? this.pagination
- : this.defaultPagination
- },
- hasPagination () {
- const pagination = this.pagination || {}
-
- return Object.keys(pagination).length > 0
- },
- hasSelectAll () {
- return this.selectAll !== undefined && this.selectAll !== false
- },
- itemsLength () {
- if (this.search) return this.searchLength
- return this.totalItems || this.items.length
- },
- indeterminate () {
- return this.hasSelectAll && this.someItems && !this.everyItem
- },
- everyItem () {
- return this.filteredItems.length &&
- this.filteredItems.every(i => this.isSelected(i))
- },
- someItems () {
- return this.filteredItems.some(i => this.isSelected(i))
- },
- getPage () {
- const { rowsPerPage } = this.computedPagination
-
- return rowsPerPage === Object(rowsPerPage)
- ? rowsPerPage.value
- : rowsPerPage
- },
- pageStart () {
- return this.getPage === -1
- ? 0
- : (this.computedPagination.page - 1) * this.getPage
- },
- pageStop () {
- return this.getPage === -1
- ? this.itemsLength
- : this.computedPagination.page * this.getPage
- },
filteredItems () {
- if (this.totalItems) return this.items
-
- let items = this.items.slice()
- const hasSearch = typeof this.search !== 'undefined' &&
- this.search !== null
-
- if (hasSearch) {
- items = this.customFilter(items, this.search, this.filter, this.headers)
- this.searchLength = items.length
- }
-
- items = this.customSort(
- items,
- this.computedPagination.sortBy,
- this.computedPagination.descending
- )
-
- return this.hideActions &&
- !this.hasPagination
- ? items
- : items.slice(this.pageStart, this.pageStop)
- },
- selected () {
- const selected = {}
- this.value.forEach(i => (selected[i[this.itemKey]] = true))
- return selected
- }
- },
-
- watch: {
- indeterminate (val) {
- if (val) this.all = true
+ return this.filteredItemsImpl(this.headers)
},
- someItems (val) {
- if (!val) this.all = false
- },
- search () {
- this.updatePagination({ page: 1, totalItems: this.itemsLength })
- },
- everyItem (val) {
- if (val) this.all = true
+ headerColumns () {
+ return this.headers.length + (this.selectAll !== false)
}
},
methods: {
- updatePagination (val) {
- const pagination = this.hasPagination
- ? this.pagination
- : this.defaultPagination
- const updatedPagination = Object.assign({}, pagination, val)
- this.$emit('update:pagination', updatedPagination)
-
- if (!this.hasPagination) {
- this.defaultPagination = updatedPagination
- }
- },
- isSelected (item) {
- return this.selected[item[this.itemKey]]
- },
- isExpanded (item) {
- return this.expanded[item[this.itemKey]]
- },
- sort (index) {
- const { sortBy, descending } = this.computedPagination
- if (sortBy === null) {
- this.updatePagination({ sortBy: index, descending: false })
- } else if (sortBy === index && !descending) {
- this.updatePagination({ descending: true })
- } else if (sortBy !== index) {
- this.updatePagination({ sortBy: index, descending: false })
- } else if (!this.mustSort) {
- this.updatePagination({ sortBy: null, descending: null })
- } else {
- this.updatePagination({ sortBy: index, descending: false })
- }
- },
- needsTR (row) {
- return row.length && row.find(c => c.tag === 'td' || c.tag === 'th')
+ hasTag (elements, tag) {
+ return Array.isArray(elements) && elements.find(e => e.tag === tag)
},
genTR (children, data = {}) {
return this.$createElement('tr', data, children)
- },
- toggle (value) {
- const selected = Object.assign({}, this.selected)
- this.filteredItems.forEach(i => (
- selected[i[this.itemKey]] = value)
- )
-
- this.$emit('input', this.items.filter(i => (
- selected[i[this.itemKey]]))
- )
}
},
@@ -321,21 +97,11 @@ export default {
? firstSortable.value
: null
- if (!this.rowsPerPageItems.length) {
- console.warn('The prop \'rows-per-page-items\' in v-data-table can not be empty.')
- } else {
- this.defaultPagination.rowsPerPage = this.rowsPerPageItems[0]
- }
-
- this.defaultPagination.totalItems = this.itemsLength
-
- this.updatePagination(
- Object.assign({}, this.defaultPagination, this.pagination)
- )
+ this.initPagination()
},
render (h) {
- return h('v-table-overflow', {}, [
+ const tableOverflow = h('v-table-overflow', {}, [
h('table', {
'class': this.classes
}, [
@@ -344,5 +110,10 @@ export default {
this.genTFoot()
])
])
+
+ return h('div', [
+ tableOverflow,
+ this.genActionsFooter()
+ ])
}
}
diff --git a/src/components/VDataTable/VDataTable.spec.js b/src/components/VDataTable/VDataTable.spec.js
index 18d628875814..81f813a79d02 100644
--- a/src/components/VDataTable/VDataTable.spec.js
+++ b/src/components/VDataTable/VDataTable.spec.js
@@ -1,15 +1,16 @@
import Vue from 'vue'
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
+import { test } from '@util/testing'
import VDataTable from './VDataTable'
-test('VDataTable.vue', () => {
+test('VDataTable.vue', ({ mount, compileToFunctions }) => {
function dataTableTestData () {
return {
propsData: {
pagination: {
descending: false,
- sortBy: 'col1'
+ sortBy: 'col1',
+ rowsPerPage: 1,
+ page: 1
},
headers: [
{ text: 'First Column', value: 'col1', class: 'a-string' },
@@ -49,15 +50,61 @@ test('VDataTable.vue', () => {
pagination.descending = true
expect(wrapper.vm.$props.pagination.descending).toBe(true)
- expect('Application is missing
component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
- it('should match a snapshot', () => {
+ it('should match a snapshot - no matching results', () => {
const data = dataTableTestData()
+ data.propsData.search = "asdf"
const wrapper = mount(VDataTable, data)
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+
+ const content = wrapper.find('table.datatable tbody > tr > td')[0]
+ expect(content.element.textContent).toBe('No matching records found')
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should match a snapshot - no data', () => {
+ const data = dataTableTestData()
+ data.propsData.items = []
+ const wrapper = mount(VDataTable, data)
+
+ expect(wrapper.html()).toMatchSnapshot()
+
+ const content = wrapper.find('table.datatable tbody > tr > td')[0]
+ expect(content.element.textContent).toBe('No data available')
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should match a snapshot - with data', () => {
+ const data = dataTableTestData()
+ data.propsData.pagination.rowsPerPage = 3
+
+ const vm = new Vue()
+ const items = props => vm.$createElement('td', [props.item.col2])
+ const component = Vue.component('test', {
+ components: {
+ VDataTable
+ },
+ render (h) {
+ return h('v-data-table', {
+ props: {
+ ...data.propsData
+ },
+ scopedSlots: {
+ items
+ }
+ })
+ }
+ })
+
+ const wrapper = mount(component)
+
+ expect(wrapper.html()).toMatchSnapshot()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should match a snapshot with single rows-per-page-items', () => {
@@ -75,7 +122,7 @@ test('VDataTable.vue', () => {
const wrapper = mount(VDataTable, data)
expect(wrapper.find('tbody td')[0].html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should match display no-results-text when no results', () => {
@@ -84,8 +131,8 @@ test('VDataTable.vue', () => {
data.propsData.search = "no such item"
const wrapper = mount(VDataTable, data)
- expect(wrapper.find('tbody td')[0].html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect(wrapper.find('tbody tr td')[0].html()).toMatchSnapshot()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render aria-sort attribute on column headers', async () => {
@@ -109,7 +156,7 @@ test('VDataTable.vue', () => {
headers.map(h => h.getAttribute('aria-sort'))
).toEqual(['none', 'none', 'ascending'])
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should match not allow a null sort', async () => {
@@ -142,7 +189,7 @@ test('VDataTable.vue', () => {
await wrapper.vm.$nextTick()
expect(wrapper.vm.defaultPagination.descending).toBe(false)
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a progress with headers slot', () => {
@@ -163,34 +210,113 @@ test('VDataTable.vue', () => {
}
}))
- expect(wrapper.find('.datatable__progress').length).toBe(1)
- expect('Application is missing component.').toHaveBeenTipped()
+ expect(wrapper.find('.datatable__progress')).toHaveLength(1)
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should only filter on data specified in headers', async () => {
const wrapper = mount(VDataTable, dataTableTestDataFilter())
- expect(wrapper.instance().filteredItems.length).toBe(1)
+ expect(wrapper.instance().filteredItems).toHaveLength(1)
wrapper.setProps({
search: 'outside'
})
- expect(wrapper.instance().filteredItems.length).toBe(0)
+ expect(wrapper.instance().filteredItems).toHaveLength(0)
wrapper.setProps({
search: 'baz'
})
- expect(wrapper.instance().filteredItems.length).toBe(1)
+ expect(wrapper.instance().filteredItems).toHaveLength(1)
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should not filter items if search is empty', async () => {
- const wrapper = mount(VDataTable, dataTableTestDataFilter())
+ const data = dataTableTestDataFilter()
+ data.propsData.search = ' '
+ const wrapper = mount(VDataTable, data)
- wrapper.setProps({
- search: ' '
- })
- expect(wrapper.instance().filteredItems.length).toBe(1)
+ expect(wrapper.instance().filteredItems).toHaveLength(data.propsData.items.length)
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should allow custom tr when using no-data slot', async () => {
+ const wrapper = mount(Vue.component('test', {
+ components: {
+ VDataTable
+ },
+ render (h) {
+ return h('v-data-table', {
+ props: {
+ items: []
+ },
+ }, [h('tr', { slot: 'no-data', class: 'custom-class' })])
+ }
+ }))
+
+ expect(wrapper.find('table tbody tr.custom-class').length).toBe(1)
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should allow custom td when using no-results slot', async () => {
+ const wrapper = mount(Vue.component('test', {
+ components: {
+ VDataTable
+ },
+ render (h) {
+ return h('v-data-table', {
+ props: {
+ items: [{}],
+ search: 'foo'
+ },
+ }, [h('td', { slot: 'no-results', class: 'custom-class' })])
+ }
+ }))
+
+ expect(wrapper.find('table tbody tr td.custom-class').length).toBe(1)
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should render tr and td when using no-results slot', async () => {
+ const wrapper = mount(Vue.component('test', {
+ components: {
+ VDataTable
+ },
+ render (h) {
+ return h('v-data-table', {
+ props: {
+ items: [{}],
+ search: 'foo'
+ },
+ }, [h('div', { slot: 'no-results', class: 'custom-class' })])
+ }
+ }))
+
+ expect(wrapper.find('table tbody tr td div.custom-class').length).toBe(1)
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should initialize everyItem state', async () => {
+ const data = dataTableTestData()
+ data.propsData.value = data.propsData.items
+ const wrapper = mount(VDataTable, data)
+
+ expect(wrapper.vm.everyItem).toBe(true);
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should update everyItem state', async () => {
+ const data = dataTableTestData()
+ data.propsData.itemKey = 'other';
+ const wrapper = mount(VDataTable, data)
+
+ expect(wrapper.vm.everyItem).toBe(false);
+ wrapper.vm.value.push(wrapper.vm.items[0]);
+ expect(wrapper.vm.everyItem).toBe(false);
- expect('Application is missing component.').toHaveBeenTipped()
+ wrapper.vm.value.push(wrapper.vm.items[1]);
+ wrapper.vm.value.push(wrapper.vm.items[2]);
+ expect(wrapper.vm.everyItem).toBe(true);
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
})
diff --git a/src/components/VDataTable/VEditDialog.js b/src/components/VDataTable/VEditDialog.js
index 482397ead0a7..4ed9c68d3798 100644
--- a/src/components/VDataTable/VEditDialog.js
+++ b/src/components/VDataTable/VEditDialog.js
@@ -1,8 +1,13 @@
-require('../../stylus/components/_small-dialog.styl')
+import '../../stylus/components/_small-dialog.styl'
+
+// Mixins
+import Returnable from '../../mixins/returnable'
export default {
name: 'v-edit-dialog',
+ mixins: [ Returnable ],
+
data () {
return {
isActive: false,
@@ -16,6 +21,7 @@ export default {
},
large: Boolean,
lazy: Boolean,
+ persistent: Boolean,
saveText: {
default: 'Save'
},
@@ -27,15 +33,7 @@ export default {
watch: {
isActive (val) {
- val &&
- this.$emit('open') &&
- setTimeout(this.focus, 50) // Give DOM time to paint
-
- if (!val) {
- !this.isSaving && this.$emit('cancel')
- this.isSaving && this.$emit('close')
- this.isSaving = false
- }
+ val && setTimeout(this.focus, 50) // Give DOM time to paint
}
},
@@ -47,11 +45,6 @@ export default {
const input = this.$refs.content.querySelector('input')
input && input.focus()
},
- save () {
- this.isSaving = true
- this.isActive = false
- this.$emit('save')
- },
genButton (fn, text) {
return this.$createElement('v-btn', {
props: {
@@ -67,15 +60,16 @@ export default {
'class': 'small-dialog__actions'
}, [
this.genButton(this.cancel, this.cancelText),
- this.genButton(this.save, this.saveText)
+ this.genButton(() => this.save(this.returnValue), this.saveText)
])
},
genContent () {
return this.$createElement('div', {
on: {
keydown: e => {
+ const input = this.$refs.content.querySelector('input')
e.keyCode === 27 && this.cancel()
- e.keyCode === 13 && this.save()
+ e.keyCode === 13 && input && this.save(input.value)
}
},
ref: 'content'
@@ -92,6 +86,7 @@ export default {
origin: 'top right',
right: true,
value: this.isActive,
+ closeOnClick: !this.persistent,
closeOnContentClick: false,
lazy: this.lazy
},
diff --git a/src/components/VDataTable/__snapshots__/VDataTable.spec.js.snap b/src/components/VDataTable/__snapshots__/VDataTable.spec.js.snap
index 48fe09631a1c..b95f42e7b8a2 100644
--- a/src/components/VDataTable/__snapshots__/VDataTable.spec.js.snap
+++ b/src/components/VDataTable/__snapshots__/VDataTable.spec.js.snap
@@ -1,299 +1,651 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`VDataTable.vue should match a snapshot 1`] = `
+exports[`VDataTable.vue should match a snapshot - no data 1`] = `
-
-
-
-
-
-
+
+
+
+
+
- arrow_upward
-
- First Column
-
-
- Second Column
-
-
-
+ arrow_upward
+
+
+
- arrow_upward
-
- Third Column
-
-
-
-
-
-
-
-
-
-
+
+ Third Column
+
+ arrow_upward
+
+
+
+
+
+
+
+
+
+
+
+ No data available
+
+
+
+
+
+
+
+
+ Rows per page:
+
- No matching records found
-
-
-
-
-
-
-
-
- Rows per page:
-
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDataTable.vue should match a snapshot - no matching results 1`] = `
+
+
+
+
+
+
+
+ First Column
+
+ arrow_upward
+
+
+
+ Second Column
+
+
+ Third Column
+
+ arrow_upward
+
+
+
+
+
+
+
+
+
+
+
+ No matching records found
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
`;
-exports[`VDataTable.vue should match a snapshot with single rows-per-page-items 1`] = `
+exports[`VDataTable.vue should match a snapshot - with data 1`] = `
-
-
-
-
-
-
+
+
+
+
+
- arrow_upward
-
- First Column
-
-
- Second Column
-
-
-
+ arrow_upward
+
+
+
- arrow_upward
-
- Third Column
-
-
-
-
-
-
-
-
-
-
- No matching records found
+ Second Column
+
+
+ Third Column
+
+ arrow_upward
+
+
+
+
+
+
+
+
+
+
+ b
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDataTable.vue should match a snapshot with single rows-per-page-items 1`] = `
+
+
+
+
+
+
+
+ First Column
+
+ arrow_upward
+
+
+
+ Second Column
+
+
+ Third Column
+
+ arrow_upward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chevron_left
+
+
+
+
+
+
+ chevron_right
+
+
+
+
+
+
`;
exports[`VDataTable.vue should match display no-data-text when no data 1`] = `
-
foo
@@ -303,7 +655,7 @@ exports[`VDataTable.vue should match display no-data-text when no data 1`] = `
exports[`VDataTable.vue should match display no-results-text when no results 1`] = `
-
bar
diff --git a/src/components/VDataTable/mixins/body.js b/src/components/VDataTable/mixins/body.js
index cd5f78935c18..f43b211c68b5 100644
--- a/src/components/VDataTable/mixins/body.js
+++ b/src/components/VDataTable/mixins/body.js
@@ -3,17 +3,7 @@ import ExpandTransitionGenerator from '../../transitions/expand-transition'
export default {
methods: {
genTBody () {
- const children = []
-
- if (!this.itemsLength && !this.items.length) {
- const noData = this.$slots['no-data'] || this.noDataText
- children.push(this.genEmptyBody(noData))
- } else if (!this.filteredItems.length) {
- const noResults = this.$slots['no-results'] || this.noResultsText
- children.push(this.genEmptyBody(noResults))
- } else {
- children.push(this.genFilteredItems())
- }
+ const children = this.genItems()
return this.$createElement('tbody', children)
},
@@ -31,7 +21,7 @@ export default {
const transition = this.$createElement('transition-group', {
class: 'datatable__expand-col',
- attrs: { colspan: '100%' },
+ attrs: { colspan: this.headerColumns },
props: {
tag: 'td'
},
@@ -40,44 +30,20 @@ export default {
return this.genTR([transition], { class: 'datatable__expand-row' })
},
- createProps (item, index) {
- const props = { item, index }
- const key = this.itemKey
-
- Object.defineProperty(props, 'selected', {
- get: () => this.selected[item[this.itemKey]],
- set: (value) => {
- let selected = this.value.slice()
- if (value) selected.push(item)
- else selected = selected.filter(i => i[key] !== item[key])
- this.$emit('input', selected)
- }
- })
-
- Object.defineProperty(props, 'expanded', {
- get: () => this.expanded[item[this.itemKey]],
- set: (value) => {
- if (!this.expand) {
- Object.keys(this.expanded).forEach((key) => {
- this.$set(this.expanded, key, false)
- })
- }
- this.$set(this.expanded, item[this.itemKey], value)
- }
- })
-
- return props
- },
genFilteredItems () {
+ if (!this.$scopedSlots.items) {
+ return null
+ }
+
const rows = []
- this.filteredItems.forEach((item, index) => {
+ for (let index = 0, len = this.filteredItems.length; index < len; ++index) {
+ const item = this.filteredItems[index]
const props = this.createProps(item, index)
- const row = this.$scopedSlots.items
- ? this.$scopedSlots.items(props)
- : []
+ const row = this.$scopedSlots.items(props)
- rows.push(this.needsTR(row)
+ rows.push(this.hasTag(row, 'td')
? this.genTR(row, {
+ key: index,
attrs: { active: this.isSelected(item) }
})
: row)
@@ -86,15 +52,23 @@ export default {
const expandRow = this.genExpandedRow(props)
rows.push(expandRow)
}
- })
+ }
return rows
},
- genEmptyBody (content) {
- return this.genTR([this.$createElement('td', {
- 'class': 'text-xs-center',
- attrs: { colspan: '100%' }
- }, content)])
+ genEmptyItems (content) {
+ if (this.hasTag(content, 'tr')) {
+ return content
+ } else if (this.hasTag(content, 'td')) {
+ return this.genTR(content)
+ } else {
+ return this.genTR([this.$createElement('td', {
+ class: {
+ 'text-xs-center': typeof content === 'string'
+ },
+ attrs: { colspan: this.headerColumns }
+ }, content)])
+ }
}
}
}
diff --git a/src/components/VDataTable/mixins/foot.js b/src/components/VDataTable/mixins/foot.js
index 92f1feeacce4..179bfd8e8ee2 100644
--- a/src/components/VDataTable/mixins/foot.js
+++ b/src/components/VDataTable/mixins/foot.js
@@ -1,128 +1,23 @@
export default {
methods: {
- genPrevIcon () {
- return this.$createElement('v-btn', {
- props: {
- disabled: this.computedPagination.page === 1,
- icon: true,
- flat: true,
- dark: this.dark,
- light: this.light
- },
- on: {
- click: () => {
- const page = this.computedPagination.page
- this.updatePagination({ page: page - 1 })
- }
- },
- attrs: {
- 'aria-label': 'Previous page' // TODO: Localization
- }
- }, [this.$createElement('v-icon', 'chevron_left')])
- },
- genNextIcon () {
- const pagination = this.computedPagination
- const disabled = pagination.rowsPerPage < 0 ||
- pagination.page * pagination.rowsPerPage >= this.itemsLength ||
- this.pageStop < 0
-
- return this.$createElement('v-btn', {
- props: {
- disabled,
- icon: true,
- flat: true,
- dark: this.dark,
- light: this.light
- },
- on: {
- click: () => {
- const page = this.computedPagination.page
- this.updatePagination({ page: page + 1 })
- }
- },
- attrs: {
- 'aria-label': 'Next page' // TODO: Localization
- }
- }, [this.$createElement('v-icon', 'chevron_right')])
- },
- genSelect () {
- return this.$createElement('div', {
- 'class': 'datatable__actions__select'
- }, [
- this.rowsPerPageText,
- this.$createElement('v-select', {
- attrs: {
- 'aria-label': this.rowsPerPageText
- },
- props: {
- items: this.rowsPerPageItems,
- value: this.computedPagination.rowsPerPage,
- hideDetails: true,
- auto: true,
- minWidth: '75px'
- },
- on: {
- input: (val) => {
- this.updatePagination({
- page: 1,
- rowsPerPage: val
- })
- }
- }
- })
- ])
- },
- genPagination () {
- let pagination = '–'
-
- if (this.itemsLength) {
- const stop = this.itemsLength < this.pageStop || this.pageStop < 0
- ? this.itemsLength
- : this.pageStop
-
- pagination = this.$scopedSlots.pageText
- ? this.$scopedSlots.pageText({
- pageStart: this.pageStart + 1,
- pageStop: stop,
- itemsLength: this.itemsLength
- })
- : `${this.pageStart + 1}-${stop} of ${this.itemsLength}`
- }
-
- return this.$createElement('div', {
- 'class': 'datatable__actions__pagination'
- }, [pagination])
- },
- genActions () {
- return [this.$createElement('div', {
- 'class': 'datatable__actions'
- }, [
- this.rowsPerPageItems.length > 1 ? this.genSelect() : null,
- this.genPagination(),
- this.genPrevIcon(),
- this.genNextIcon()
- ])]
- },
genTFoot () {
- const children = []
-
- if (this.$slots.footer) {
- const footer = this.$slots.footer
- const row = this.needsTR(footer) ? this.genTR(footer) : footer
-
- children.push(row)
+ if (!this.$slots.footer) {
+ return null
}
- if (!this.hideActions) {
- children.push(this.genTR([
- this.$createElement('td', {
- attrs: { colspan: '100%' }
- }, this.genActions())
- ]))
+ const footer = this.$slots.footer
+ const row = this.hasTag(footer, 'td') ? this.genTR(footer) : footer
+
+ return this.$createElement('tfoot', [row])
+ },
+ genActionsFooter () {
+ if (this.hideActions) {
+ return null
}
- if (!children.length) return null
- return this.$createElement('tfoot', children)
+ return this.$createElement('div', {
+ 'class': this.classes
+ }, this.genActions())
}
}
}
diff --git a/src/components/VDataTable/mixins/head.js b/src/components/VDataTable/mixins/head.js
index 92da2e75f340..d92c6ff2b785 100644
--- a/src/components/VDataTable/mixins/head.js
+++ b/src/components/VDataTable/mixins/head.js
@@ -1,4 +1,13 @@
+import { consoleWarn } from '../../../util/console'
+
export default {
+ props: {
+ sortIcon: {
+ type: String,
+ default: 'arrow_upward'
+ }
+ },
+
methods: {
genTHead () {
if (this.hideHeaders) return // Exit Early since no headers are needed.
@@ -9,10 +18,10 @@ export default {
const row = this.$scopedSlots.headers({
headers: this.headers,
indeterminate: this.indeterminate,
- all: this.all
+ all: this.everyItem
})
- children = [this.needsTR(row) ? this.genTR(row) : row, this.genTProgress()]
+ children = [this.hasTag(row, 'th') ? this.genTR(row) : row, this.genTProgress()]
} else {
const row = this.headers.map(o => this.genHeader(o))
const checkbox = this.$createElement('v-checkbox', {
@@ -21,7 +30,7 @@ export default {
light: this.light,
color: this.selectAll === true ? '' : this.selectAll,
hideDetails: true,
- inputValue: this.all,
+ inputValue: this.everyItem,
indeterminate: this.indeterminate
},
on: { change: this.toggle }
@@ -46,6 +55,7 @@ export default {
genHeaderData (header, children) {
const classes = ['column']
const data = {
+ key: header[this.headerText],
attrs: {
role: 'columnheader',
scope: 'col',
@@ -55,13 +65,13 @@ export default {
}
}
- if ('sortable' in header && header.sortable || !('sortable' in header)) {
+ if (header.sortable == null || header.sortable) {
this.genHeaderSortingData(header, children, data, classes)
} else {
data.attrs['aria-label'] += ': Not sorted.' // TODO: Localization
}
- classes.push(`text-xs-${header.align || 'right'}`)
+ classes.push(`text-xs-${header.align || 'left'}`)
if (Array.isArray(header.class)) {
classes.push(...header.class)
} else if (header.class) {
@@ -73,7 +83,7 @@ export default {
},
genHeaderSortingData (header, children, data, classes) {
if (!('value' in header)) {
- console.warn('Data table headers must have a value property that corresponds to a value in the v-model array')
+ consoleWarn('Headers must have a value property that corresponds to a value in the v-model array', this)
}
data.attrs.tabIndex = 0
@@ -92,8 +102,12 @@ export default {
}
classes.push('sortable')
- const icon = this.$createElement('v-icon', 'arrow_upward')
- if (header.align && header.align === 'left') {
+ const icon = this.$createElement('v-icon', {
+ props: {
+ small: true
+ }
+ }, this.sortIcon)
+ if (!header.align || header.align === 'left') {
children.push(icon)
} else {
children.unshift(icon)
diff --git a/src/components/VDataTable/mixins/progress.js b/src/components/VDataTable/mixins/progress.js
index 209fd7b08247..68c208f06261 100644
--- a/src/components/VDataTable/mixins/progress.js
+++ b/src/components/VDataTable/mixins/progress.js
@@ -4,7 +4,7 @@ export default {
const col = this.$createElement('th', {
staticClass: 'column',
attrs: {
- colspan: '100%'
+ colspan: this.headerColumns
}
}, [this.genProgress()])
diff --git a/src/components/VDatePicker/VDatePicker.date.spec.js b/src/components/VDatePicker/VDatePicker.date.spec.js
new file mode 100644
index 000000000000..fff630597cb3
--- /dev/null
+++ b/src/components/VDatePicker/VDatePicker.date.spec.js
@@ -0,0 +1,498 @@
+import Vue from 'vue'
+import { test, touch } from '@util/testing'
+import VDatePicker from './VDatePicker'
+import VMenu from '@components/VMenu'
+
+test('VDatePicker.js', ({ mount, compileToFunctions }) => {
+ it('should display the correct date in title and header', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ }
+ })
+
+ const title = wrapper.find('.date-picker-title__date')[0]
+ const header = wrapper.find('.date-picker-header__value strong')[0]
+
+ expect(title.text()).toBe('Tue, Nov 1')
+ expect(header.text()).toBe('November 2005')
+ })
+
+ it('should match snapshot with default settings', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render readonly picker', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ readonly: true
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should emit input event on date click', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ const change = jest.fn()
+ wrapper.vm.$on('change', change)
+
+ wrapper.find('.date-picker-table--date tbody tr+tr td:first-child button')[0].trigger('click')
+ expect(input).toBeCalledWith('2013-05-05')
+ expect(change).toBeCalledWith('2013-05-05')
+ })
+
+ it('should not emit input event on month click if date is not allowed', async () => {
+ const cb = jest.fn()
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-13',
+ allowedDates: () => false
+ },
+ data: {
+ activePicker: 'MONTH'
+ }
+ })
+
+ wrapper.vm.$on('input', cb);
+ wrapper.find('.date-picker-table--month button')[0].trigger('click')
+ expect(cb).not.toBeCalled()
+ })
+
+ it('should emit input event on year click (reactive picker)', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-13',
+ reactive: true
+ },
+ data: {
+ activePicker: 'YEAR'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input);
+
+ const change = jest.fn()
+ wrapper.vm.$on('change', input);
+
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(input).toBeCalledWith('2012-05-13')
+ expect(change).not.toBeCalled()
+ })
+
+ it('should not emit input event on year click if date is not allowed', async () => {
+ const cb = jest.fn()
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-13',
+ allowedDates: () => false
+ },
+ data: {
+ activePicker: 'YEAR'
+ }
+ })
+
+ wrapper.vm.$on('input', cb);
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(cb).not.toBeCalled()
+ })
+
+ it('should be scrollable', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ scrollable: true
+ }
+ })
+
+ wrapper.find('.date-picker-table--date')[0].trigger('wheel')
+ expect(wrapper.vm.tableDate).toBe('2013-06')
+ })
+
+ it('should change tableDate on touch', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ scrollable: true
+ }
+ })
+
+ const table = wrapper.find('.date-picker-table--date')[0]
+ touch(table).start(0, 0).end(20, 0)
+ expect(wrapper.vm.tableDate).toBe('2013-04')
+
+ touch(table).start(0, 0).end(-20, 0)
+ expect(wrapper.vm.tableDate).toBe('2013-06')
+ })
+
+ it('should match snapshot with dark theme', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ dark: true
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with no title', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ noTitle: true
+ }
+ })
+
+ expect(wrapper.find('.picker__title')).toHaveLength(0)
+ })
+
+ it('should pass first day of week to date-picker-table component', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ firstDayOfWeek: 2
+ }
+ })
+
+ expect(wrapper.vm.$refs.table.firstDayOfWeek).toBe(2)
+ })
+
+ // TODO: This fails in different ways for multiple people
+ // Avoriaz/Jsdom (?) doesn't fully support date formatting using locale
+ // This should be tested in browser env
+ it.skip('should match snapshot with locale', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ locale: 'fa-AF'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with title/header formatting functions', () => {
+ const dateFormat = date => `(${date})`
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ headerDateFormat: dateFormat,
+ titleDateFormat: dateFormat
+ }
+ })
+
+ expect(wrapper.find('.date-picker-title__date')[0].text()).toBe('(2005-11-01)')
+ expect(wrapper.find('.date-picker-header__value')[0].text()).toBe('(2005-11)')
+ })
+
+ it('should match snapshot with colored picker', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ color: 'primary',
+ headerColor: 'orange darken-1'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with colored picker', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ color: 'orange darken-1'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with year icon', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ yearIcon: 'year'
+ }
+ })
+
+ expect(wrapper.find('.picker__title')[0].html()).toMatchSnapshot()
+ })
+
+ it('should match change month when clicked on header arrow buttons', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01'
+ }
+ })
+
+ const [leftButton, rightButton] = wrapper.find('.date-picker-header button')
+
+ leftButton.trigger('click')
+ expect(wrapper.vm.tableDate).toBe('2005-10')
+
+ rightButton.trigger('click')
+ expect(wrapper.vm.tableDate).toBe('2005-12')
+ })
+
+ it('should match change active picker when clicked on month button', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01'
+ }
+ })
+
+ const button = wrapper.find('.date-picker-header strong')[0]
+
+ button.trigger('click')
+ expect(wrapper.vm.activePicker).toBe('MONTH')
+ })
+
+ it('should match snapshot with slot', async () => {
+ const vm = new Vue()
+ const slot = props => vm.$createElement('div', { class: 'scoped-slot' })
+ const component = Vue.component('test', {
+ components: {
+ VDatePicker
+ },
+ render (h) {
+ return h('v-date-picker', {
+ props: {
+ type: 'date',
+ value: '2005-11-01'
+ },
+ scopedSlots: {
+ default: slot
+ }
+ })
+ }
+ })
+
+ const wrapper = mount(component)
+ expect(wrapper.find('.picker__actions .scoped-slot')).toHaveLength(1)
+ })
+
+ it('should match years snapshot', async () => {
+ const wrapper = mount(VDatePicker, {
+ data: {
+ activePicker: 'YEAR'
+ },
+ propsData: {
+ type: 'date',
+ value: '2005-11-01'
+ }
+ })
+
+ expect(wrapper.vm.activePicker).toBe('YEAR')
+
+ wrapper.find('.date-picker-title__date')[0].trigger('click')
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.activePicker).toBe('DATE')
+
+ wrapper.find('.date-picker-title__year')[0].trigger('click')
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.activePicker).toBe('YEAR')
+ })
+
+ it('should select year', async () => {
+ const wrapper = mount(VDatePicker, {
+ data: {
+ activePicker: 'YEAR'
+ },
+ propsData: {
+ type: 'date',
+ value: '2005-11-01'
+ }
+ })
+
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(wrapper.vm.activePicker).toBe('MONTH')
+ expect(wrapper.vm.tableDate).toBe('2004-11')
+ })
+
+ it('should set the table date when value has changed', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: null
+ }
+ })
+
+ wrapper.setProps({ value: '2005-11-11' })
+ expect(wrapper.vm.tableDate).toBe('2005-11')
+ })
+
+ it('should update the active picker if type has changed', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '1999-12-13',
+ type: 'date'
+ }
+ })
+
+ wrapper.vm.$on('input', value => wrapper.setProps({ value }))
+
+ wrapper.setProps({ type: 'month' })
+ expect(wrapper.vm.activePicker).toBe('MONTH')
+ expect(wrapper.vm.value).toBe('1999-12')
+ // TODO: uncomment when type: 'year' is implemented
+ // wrapper.setProps({ type: 'year' })
+ // expect(wrapper.vm.activePicker).toBe('YEAR')
+ // expect(wrapper.vm.inputDate).toBe('1999')
+ // wrapper.setProps({ type: 'month' })
+ // expect(wrapper.vm.activePicker).toBe('MONTH')
+ // expect(wrapper.vm.inputDate).toBe('1999-01')
+ wrapper.setProps({ type: 'date' })
+ expect(wrapper.vm.activePicker).toBe('DATE')
+ expect(wrapper.vm.value).toBe('1999-12-01')
+ })
+
+ it('should format title date', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ }
+ })
+
+ expect(wrapper.vm.defaultTitleDateFormatter('2013-03-05')).toBe('Tue, Mar 5')
+
+ wrapper.setProps({ landscape: true })
+ expect(wrapper.vm.defaultTitleDateFormatter('2013-03-05')).toBe('Tue, Mar 5')
+ })
+
+ it('should use prev and next icons', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ prevIcon: 'block',
+ nextIcon: 'check'
+ }
+ })
+
+ const icons = wrapper.find('.date-picker-header .icon')
+ expect(icons[0].element.textContent).toBe('block')
+ expect(icons[1].element.textContent).toBe('check')
+ })
+
+ it('should emit update:pickerDate event when tableDate changes', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2017-09'
+ }
+ })
+
+ const pickerDate = jest.fn()
+ wrapper.vm.$on('update:pickerDate', pickerDate)
+ wrapper.vm.tableDate = '2013-11'
+ await wrapper.vm.$nextTick()
+ expect(pickerDate).toBeCalledWith('2013-11')
+ })
+
+ it('should set tableDate to pickerDate if provided', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2017-09',
+ pickerDate: '2013-11'
+ }
+ })
+
+ expect(wrapper.vm.tableDate).toBe('2013-11')
+ })
+
+ it('should update pickerDate to the selected month after setting it to null', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2017-09-13',
+ pickerDate: '2013-11'
+ }
+ })
+
+ const update = jest.fn()
+ wrapper.vm.$on('update:pickerDate', update)
+ await wrapper.vm.$nextTick()
+
+ wrapper.setProps({
+ pickerDate: null
+ })
+ await wrapper.vm.$nextTick()
+ expect(update).toBeCalledWith('2017-09')
+ })
+
+ it('should render component with min/max props', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-01-07',
+ min: '2013-01-03',
+ max: '2013-01-17'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.vm.activePicker = 'MONTH'
+ await wrapper.vm.$nextTick()
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.vm.activePicker = 'YEAR'
+ await wrapper.vm.$nextTick()
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should emit @input and not emit @change when month is clicked (not reative picker)', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-02-07',
+ reactive: true
+ },
+ data: {
+ activePicker: 'MONTH'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ const change = jest.fn()
+ wrapper.vm.$on('change', change)
+
+ wrapper.find('tbody tr td button')[0].trigger('click')
+ wrapper.vm.$nextTick()
+ expect(change).not.toBeCalled()
+ expect(input).toBeCalledWith('2013-01-07')
+ })
+
+ it('should not emit @input and not emit @change when month is clicked (lazy picker)', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-02-07'
+ },
+ data: {
+ activePicker: 'MONTH'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ const change = jest.fn()
+ wrapper.vm.$on('change', change)
+
+ wrapper.find('tbody tr td button')[0].trigger('click')
+ wrapper.vm.$nextTick()
+ expect(change).not.toBeCalled()
+ expect(input).not.toBeCalled()
+ })
+})
diff --git a/src/components/VDatePicker/VDatePicker.js b/src/components/VDatePicker/VDatePicker.js
index 67c488f85440..e00dc4ce422d 100644
--- a/src/components/VDatePicker/VDatePicker.js
+++ b/src/components/VDatePicker/VDatePicker.js
@@ -1,21 +1,19 @@
-require('../../stylus/components/_pickers.styl')
-require('../../stylus/components/_date-picker.styl')
-
-import { createRange } from '../../util/helpers'
-
-import DateYears from './mixins/date-years'
-import DateTitle from './mixins/date-title'
-import DateHeader from './mixins/date-header'
-import DateTable from './mixins/date-table'
-import MonthTable from './mixins/month-table'
-import Picker from '../../mixins/picker'
+// Components
import VBtn from '../VBtn'
import VCard from '../VCard'
import VIcon from '../VIcon'
+import VDatePickerTitle from './VDatePickerTitle'
+import VDatePickerHeader from './VDatePickerHeader'
+import VDatePickerDateTable from './VDatePickerDateTable'
+import VDatePickerMonthTable from './VDatePickerMonthTable'
+import VDatePickerYears from './VDatePickerYears'
-import Touch from '../../directives/touch'
+// Mixins
+import Picker from '../../mixins/picker'
-const pad = n => (n * 1 < 10) ? `0${n * 1}` : `${n}`
+// Utils
+import { pad, createNativeLocaleFormatter } from './util'
+import isDateAllowed from './util/isDateAllowed'
export default {
name: 'v-date-picker',
@@ -23,39 +21,54 @@ export default {
components: {
VBtn,
VCard,
- VIcon
+ VIcon,
+ VDatePickerTitle,
+ VDatePickerHeader,
+ VDatePickerDateTable,
+ VDatePickerMonthTable,
+ VDatePickerYears
},
- mixins: [Picker, DateYears, DateTitle, DateHeader, DateTable, MonthTable],
-
- directives: { Touch },
+ mixins: [Picker],
data () {
const now = new Date()
return {
activePicker: this.type.toUpperCase(),
- currentDay: null,
- currentMonth: null,
- currentYear: null,
+ defaultColor: 'accent',
+ inputDay: null,
+ inputMonth: null,
+ inputYear: null,
isReversing: false,
- originalDate: this.value,
+ now,
// tableDate is a string in 'YYYY' / 'YYYY-M' format (leading zero for month is not required)
- tableDate: this.type === 'month'
- ? `${now.getFullYear()}`
- : `${now.getFullYear()}-${now.getMonth() + 1}`
+ tableDate: (() => {
+ if (this.pickerDate) {
+ return this.pickerDate
+ }
+
+ const date = this.value || `${now.getFullYear()}-${now.getMonth() + 1}`
+ const type = this.type === 'date' ? 'month' : 'year'
+ return this.sanitizeDateString(date, type)
+ })()
}
},
props: {
- allowedDates: {
- type: [Array, Object, Function],
- default: () => (null)
- },
+ allowedDates: Function,
// Function formatting the day in date picker table
dayFormat: {
type: Function,
default: null
},
+ events: {
+ type: [Array, Object, Function],
+ default: () => null
+ },
+ eventColor: {
+ type: [String, Function, Object],
+ default: 'warning'
+ },
firstDayOfWeek: {
type: [String, Number],
default: 0
@@ -69,11 +82,29 @@ export default {
type: String,
default: 'en-us'
},
+ max: String,
+ min: String,
// Function formatting month in the months table
monthFormat: {
type: Function,
default: null
},
+ nextIcon: {
+ type: String,
+ default: 'chevron_right'
+ },
+ pickerDate: String,
+ prevIcon: {
+ type: String,
+ default: 'chevron_left'
+ },
+ reactive: Boolean,
+ readonly: Boolean,
+ scrollable: Boolean,
+ showCurrent: {
+ type: [Boolean, String],
+ default: true
+ },
// Function formatting currently selected date in the picker title
titleDateFormat: {
type: Function,
@@ -82,7 +113,7 @@ export default {
type: {
type: String,
default: 'date',
- validator: type => ['date', 'month'/*, 'year'*/].includes(type)
+ validator: type => ['date', 'month'].includes(type) // TODO: year
},
value: String,
// Function formatting the year in table header and pickup title
@@ -94,89 +125,39 @@ export default {
},
computed: {
- weekDays () {
- const first = parseInt(this.firstDayOfWeek, 10)
-
- return this.formatters.weekDay
- ? createRange(7).map(i => this.formatters.weekDay(`2017-01-${first + i + 15}`)) // 2017-01-15 is Sunday
- : createRange(7).map(i => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][(i + first) % 7])
- },
- firstAllowedDate () {
- const now = new Date()
- const year = now.getFullYear()
- const month = now.getMonth()
-
- if (this.allowedDates) {
- for (let date = now.getDate(); date <= 31; date++) {
- const dateString = `${year}-${month + 1}-${date}`
- if (isNaN(new Date(dateString).getDate())) break
-
- const sanitizedDateString = this.sanitizeDateString(dateString, 'date')
- if (this.isAllowed(sanitizedDateString)) {
- return sanitizedDateString
- }
- }
+ current () {
+ if (this.showCurrent === true) {
+ return this.sanitizeDateString(`${this.now.getFullYear()}-${this.now.getMonth() + 1}-${this.now.getDate()}`, this.type)
}
- return this.sanitizeDateString(`${year}-${month + 1}-${now.getDate()}`, 'date')
+ return this.showCurrent || null
},
- firstAllowedMonth () {
- const now = new Date()
- const year = now.getFullYear()
-
- if (this.allowedDates) {
- for (let month = now.getMonth(); month < 12; month++) {
- const dateString = `${year}-${month + 1}`
- const sanitizedDateString = this.sanitizeDateString(dateString, 'month')
- if (this.isAllowed(sanitizedDateString)) {
- return sanitizedDateString
- }
- }
- }
-
- return this.sanitizeDateString(`${year}-${now.getMonth() + 1}`, 'month')
- },
- // inputDate MUST be a string in ISO 8601 format (including leading zero for month/day)
- // YYYY-MM for month picker
- // YYYY-MM-DD for date picker
- inputDate: {
- get () {
- if (this.value) {
- return this.sanitizeDateString(this.value, this.type)
- }
-
- return this.type === 'month' ? this.firstAllowedMonth : this.firstAllowedDate
- },
- set (value) {
- const date = value == null ? this.originalDate : this.sanitizeDateString(value, this.type)
- this.$emit('input', date)
- }
+ inputDate () {
+ return this.type === 'date'
+ ? `${this.inputYear}-${pad(this.inputMonth + 1)}-${pad(this.inputDay)}`
+ : `${this.inputYear}-${pad(this.inputMonth + 1)}`
},
- day () {
- return this.inputDate.split('-')[2] * 1
+ tableMonth () {
+ return (this.pickerDate || this.tableDate).split('-')[1] - 1
},
- month () {
- return this.inputDate.split('-')[1] - 1
+ tableYear () {
+ return (this.pickerDate || this.tableDate).split('-')[0] * 1
},
- year () {
- return this.inputDate.split('-')[0] * 1
+ minMonth () {
+ return this.min ? this.sanitizeDateString(this.min, 'month') : null
},
- tableMonth () {
- return this.tableDate.split('-')[1] - 1
+ maxMonth () {
+ return this.max ? this.sanitizeDateString(this.max, 'month') : null
},
- tableYear () {
- return this.tableDate.split('-')[0] * 1
+ minYear () {
+ return this.min ? this.sanitizeDateString(this.min, 'year') : null
},
- computedTransition () {
- return this.isReversing ? 'tab-reverse-transition' : 'tab-transition'
+ maxYear () {
+ return this.max ? this.sanitizeDateString(this.max, 'year') : null
},
formatters () {
return {
- day: this.dayFormat || this.createNativeLocaleFormatter(this.locale, { day: 'numeric', timeZone: 'UTC' }, { start: 8, length: 2 }),
- headerDate: this.headerDateFormat || this.createNativeLocaleFormatter(this.locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }, { length: 7 }),
- month: this.monthFormat || this.createNativeLocaleFormatter(this.locale, { month: 'short', timeZone: 'UTC' }, { start: 5, length: 2 }),
- year: this.yearFormat || this.createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
- weekDay: this.createNativeLocaleFormatter(this.locale, { weekday: 'narrow', timeZone: 'UTC' }),
+ year: this.yearFormat || createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
titleDate: this.titleDateFormat || this.defaultTitleDateFormatter
}
},
@@ -187,7 +168,7 @@ export default {
date: { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }
}
- const titleDateFormatter = this.createNativeLocaleFormatter(this.locale, titleFormats[this.type], {
+ const titleDateFormatter = createNativeLocaleFormatter(this.locale, titleFormats[this.type], {
start: 0,
length: { date: 10, month: 7, year: 4 }[this.type]
})
@@ -201,188 +182,215 @@ export default {
},
watch: {
- activePicker (val, prev) {
- if (val !== 'YEAR') return
-
- // That's a quirk, setting timeout stopped working after fixing #1649
- // It worked but for timeouts significantly longer than the transition duration
- const interval = setInterval(() => {
- if (this.$refs.years) {
- this.$refs.years.scrollTop = this.$refs.years.scrollHeight / 2 - 125
- clearInterval(interval)
- }
- }, 100)
- },
tableDate (val, prev) {
// Make a ISO 8601 strings from val and prev for comparision, otherwise it will incorrectly
// compare for example '2000-9' and '2000-10'
const sanitizeType = this.type === 'month' ? 'year' : 'month'
this.isReversing = this.sanitizeDateString(val, sanitizeType) < this.sanitizeDateString(prev, sanitizeType)
+ this.$emit('update:pickerDate', val)
},
- value (val) {
+ pickerDate (val) {
if (val) {
- this.tableDate = this.type === 'month' ? `${this.year}` : `${this.year}-${this.month + 1}`
+ this.tableDate = val
+ } else if (this.value && this.type === 'date') {
+ this.tableDate = this.sanitizeDateString(this.value, 'month')
+ } else if (this.value && this.type === 'month') {
+ this.tableDate = this.sanitizeDateString(this.value, 'year')
+ }
+ },
+ value () {
+ this.setInputDate()
+ if (this.value && !this.pickerDate) {
+ this.tableDate = this.sanitizeDateString(this.inputDate, this.type === 'month' ? 'year' : 'month')
}
},
- type (val) {
- if (val === 'month' && this.activePicker === 'DATE') {
- this.activePicker = 'MONTH'
- } else if (val === 'year') {
- this.activePicker = 'YEAR'
+ type (type) {
+ this.activePicker = type.toUpperCase()
+
+ if (this.value) {
+ const date = this.sanitizeDateString(this.value, type)
+ this.$emit('input', this.isDateAllowed(date) ? date : null)
}
}
},
methods: {
- save () {
- if (this.originalDate) {
- this.originalDate = this.value
+ isDateAllowed (value) {
+ return isDateAllowed(value, this.min, this.max, this.allowedDates)
+ },
+ yearClick (value) {
+ this.inputYear = value
+ if (this.type === 'month') {
+ this.tableDate = `${value}`
} else {
- this.originalDate = this.inputDate
+ this.tableDate = `${value}-${pad(this.tableMonth + 1)}`
}
-
- if (this.$parent && this.$parent.isActive) this.$parent.isActive = false
- },
- cancel () {
- this.inputDate = this.originalDate
- if (this.$parent && this.$parent.isActive) this.$parent.isActive = false
+ this.activePicker = 'MONTH'
+ this.reactive && this.isDateAllowed(this.inputDate) && this.$emit('input', this.inputDate)
},
- isAllowed (date) {
- if (!this.allowedDates) return true
-
- // date parameter must be in ISO 8601 format with leading zero
- // If allowedDates is an array its values must be in ISO 8601 format with leading zero
- // If allowedDates is on object its min/max properties must be in ISO 8601 with leading zero
- if (Array.isArray(this.allowedDates)) {
- return this.allowedDates.indexOf(date) > -1
- } else if (this.allowedDates instanceof Function) {
- return this.allowedDates(date)
- } else if (this.allowedDates instanceof Object) {
- const min = this.allowedDates.min
- const max = this.allowedDates.max
- return (!min || min <= date) && (!max || max >= date)
+ monthClick (value) {
+ this.inputYear = parseInt(value.split('-')[0], 10)
+ this.inputMonth = parseInt(value.split('-')[1], 10) - 1
+ if (this.type === 'date') {
+ this.tableDate = value
+ this.activePicker = 'DATE'
+ this.reactive && this.isDateAllowed(this.inputDate) && this.$emit('input', this.inputDate)
+ } else {
+ this.$emit('input', this.inputDate)
+ this.$emit('change', this.inputDate)
}
-
- return true
},
- genTableTouch (touchCallback) {
- return {
- name: 'touch',
- value: {
- left: e => (e.offsetX < -15) && touchCallback(1),
- right: e => (e.offsetX > 15) && touchCallback(-1)
+ dateClick (value) {
+ this.inputYear = parseInt(value.split('-')[0], 10)
+ this.inputMonth = parseInt(value.split('-')[1], 10) - 1
+ this.inputDay = parseInt(value.split('-')[2], 10)
+ this.$emit('input', this.inputDate)
+ this.$emit('change', this.inputDate)
+ },
+ genPickerTitle () {
+ return this.$createElement('v-date-picker-title', {
+ props: {
+ date: this.value ? this.formatters.titleDate(this.value) : '',
+ selectingYear: this.activePicker === 'YEAR',
+ year: this.formatters.year(`${this.inputYear}`),
+ yearIcon: this.yearIcon,
+ value: this.value
+ },
+ slot: 'title',
+ style: this.readonly ? {
+ 'pointer-events': 'none'
+ } : undefined,
+ on: {
+ 'update:selectingYear': value => this.activePicker = value ? 'YEAR' : this.type.toUpperCase()
}
- }
+ })
},
- genTable (tableChildren, touchCallback) {
- const wheel = this.activePicker === 'MONTH' ? this.monthWheelScroll : this.dateWheelScroll
- const options = {
- staticClass: 'picker--date__table',
- 'class': {
- 'picker--month__table': this.activePicker === 'MONTH'
+ genTableHeader () {
+ return this.$createElement('v-date-picker-header', {
+ props: {
+ nextIcon: this.nextIcon,
+ color: this.color,
+ disabled: this.readonly,
+ format: this.headerDateFormat,
+ locale: this.locale,
+ min: this.activePicker === 'DATE' ? this.minMonth : this.minYear,
+ max: this.activePicker === 'DATE' ? this.maxMonth : this.maxYear,
+ prevIcon: this.prevIcon,
+ value: this.activePicker === 'DATE' ? `${this.tableYear}-${pad(this.tableMonth + 1)}` : `${this.tableYear}`
},
- on: this.scrollable ? { wheel } : undefined,
- directives: [this.genTableTouch(touchCallback)]
- }
-
- const table = this.$createElement('table', {
- key: this.activePicker === 'MONTH' ? this.tableYear : this.tableMonth
- }, tableChildren)
-
- return this.$createElement('div', options, [
- this.$createElement('transition', {
- props: { name: this.computedTransition }
- }, [table])
- ])
- },
- genPickerBody (h) {
- const pickerBodyChildren = []
- if (this.activePicker === 'DATE') {
- pickerBodyChildren.push(h('div', { staticClass: 'picker--date__header' }, [this.genSelector()]))
- pickerBodyChildren.push(this.genTable([
- this.dateGenTHead(),
- this.dateGenTBody()
- ], value => this.updateTableMonth(this.tableMonth + value)))
- } else if (this.activePicker === 'MONTH') {
- pickerBodyChildren.push(h('div', { staticClass: 'picker--date__header' }, [this.genSelector()]))
- pickerBodyChildren.push(this.genTable([
- this.monthGenTBody()
- ], value => this.tableDate = `${this.tableYear + value}`))
- } else if (this.activePicker === 'YEAR') {
- pickerBodyChildren.push(this.genYears())
- }
-
- return pickerBodyChildren
+ on: {
+ toggle: () => this.activePicker = (this.activePicker === 'DATE' ? 'MONTH' : 'YEAR'),
+ input: value => this.tableDate = value
+ }
+ })
},
- createNativeLocaleFormatter (locale, options, { start, length } = { start: 0, length: 0 }) {
- const makeIsoString = dateString => {
- const [year, month, date] = dateString.trim().split(' ')[0].split('-')
- return [year, pad(month || 1), pad(date || 1)].join('-')
- }
-
- try {
- const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options)
- return dateString => intlFormatter.format(new Date(`${makeIsoString(dateString)}T00:00:00+00:00`))
- } catch (e) {
- return (start || length) ? dateString => makeIsoString(dateString).substr(start, length) : null
- }
+ genDateTable () {
+ return this.$createElement('v-date-picker-date-table', {
+ props: {
+ allowedDates: this.allowedDates,
+ color: this.color,
+ current: this.current,
+ disabled: this.readonly,
+ events: this.events,
+ eventColor: this.eventColor,
+ firstDayOfWeek: this.firstDayOfWeek,
+ format: this.dayFormat,
+ locale: this.locale,
+ min: this.min,
+ max: this.max,
+ tableDate: `${this.tableYear}-${pad(this.tableMonth + 1)}`,
+ scrollable: this.scrollable,
+ value: this.value
+ },
+ ref: 'table',
+ on: {
+ input: this.dateClick,
+ tableDate: value => this.tableDate = value
+ }
+ })
+ },
+ genMonthTable () {
+ return this.$createElement('v-date-picker-month-table', {
+ props: {
+ allowedDates: this.type === 'month' ? this.allowedDates : null,
+ color: this.color,
+ current: this.current ? this.sanitizeDateString(this.current, 'month') : null,
+ disabled: this.readonly,
+ format: this.monthFormat,
+ locale: this.locale,
+ min: this.minMonth,
+ max: this.maxMonth,
+ scrollable: this.scrollable,
+ value: (!this.value || this.type === 'month') ? this.value : this.value.substr(0, 7),
+ tableDate: `${this.tableYear}`
+ },
+ ref: 'table',
+ on: {
+ input: this.monthClick,
+ tableDate: value => this.tableDate = value
+ }
+ })
+ },
+ genYears () {
+ return this.$createElement('v-date-picker-years', {
+ props: {
+ color: this.color,
+ format: this.yearFormat,
+ locale: this.locale,
+ min: this.minYear,
+ max: this.maxYear,
+ value: `${this.tableYear}`
+ },
+ on: {
+ input: this.yearClick
+ }
+ })
+ },
+ genPickerBody () {
+ const children = this.activePicker === 'YEAR' ? [
+ this.genYears()
+ ] : [
+ this.genTableHeader(),
+ this.activePicker === 'DATE' ? this.genDateTable() : this.genMonthTable()
+ ]
+
+ return this.$createElement('div', {
+ key: this.activePicker,
+ style: this.readonly ? {
+ 'pointer-events': 'none'
+ } : undefined
+ }, children)
},
// Adds leading zero to month/day if necessary, returns 'YYYY' if type = 'year',
// 'YYYY-MM' if 'month' and 'YYYY-MM-DD' if 'date'
sanitizeDateString (dateString, type) {
- const [year, month, date] = dateString.split('-')
+ const [year, month = 1, date = 1] = dateString.split('-')
return `${year}-${pad(month)}-${pad(date)}`.substr(0, { date: 10, month: 7, year: 4 }[type])
},
- // For month = 12 it sets the tableDate to January next year
- // For month = -1 it sets the tableDate to December previous year
- // Otherwise it just changes the table month
- updateTableMonth (month /* -1..12 */) {
- if (month === 12) {
- this.tableDate = `${this.tableYear + 1}-01`
- } else if (month === -1) {
- this.tableDate = `${this.tableYear - 1}-12`
+ setInputDate () {
+ if (this.value) {
+ const array = this.value.split('-')
+ this.inputYear = parseInt(array[0], 10)
+ this.inputMonth = parseInt(array[1], 10) - 1
+ if (this.type === 'date') {
+ this.inputDay = parseInt(array[2], 10)
+ }
} else {
- this.tableDate = `${this.tableYear}-${month + 1}`
+ this.inputYear = this.inputYear || this.now.getFullYear()
+ this.inputMonth = this.inputMonth == null ? this.inputMonth : this.now.getMonth()
+ this.inputDay = this.inputDay || this.now.getDate()
}
}
},
created () {
- this.tableDate = this.type === 'month' ? `${this.year}` : `${this.year}-${this.month + 1}`
- },
-
- mounted () {
- const date = new Date()
- this.currentDay = date.getDate()
- this.currentMonth = date.getMonth()
- this.currentYear = date.getFullYear()
+ if (this.pickerDate !== this.tableDate) {
+ this.$emit('update:pickerDate', this.tableDate)
+ }
+ this.setInputDate()
},
render (h) {
- const children = []
-
- !this.noTitle && children.push(this.genTitle(this.formatters.titleDate(this.inputDate)))
-
- children.push(h('transition', {
- props: {
- origin: 'center center',
- mode: 'out-in',
- name: 'scale-transition'
- }
- }, [h('div', {
- staticClass: 'picker__body',
- key: this.activePicker
- }, this.genPickerBody(h))]))
-
- this.$scopedSlots.default && children.push(this.genSlot())
-
- return h('v-card', {
- staticClass: 'picker picker--date',
- 'class': {
- 'picker--landscape': this.landscape,
- ...this.themeClasses
- }
- }, children)
+ return this.genPicker('picker--date')
}
-
}
diff --git a/src/components/VDatePicker/VDatePicker.month.spec.js b/src/components/VDatePicker/VDatePicker.month.spec.js
new file mode 100755
index 000000000000..827d7fb40ed3
--- /dev/null
+++ b/src/components/VDatePicker/VDatePicker.month.spec.js
@@ -0,0 +1,208 @@
+import Vue from 'vue'
+import { test } from '@util/testing'
+import VDatePicker from './VDatePicker'
+import VMenu from '@components/VMenu'
+
+test('VDatePicker.js', ({ mount, compileToFunctions }) => {
+ it('should emit input event on year click (reactive picker)', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05',
+ type: 'month',
+ reactive: true
+ },
+ data: {
+ activePicker: 'YEAR'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input);
+
+ const change = jest.fn()
+ wrapper.vm.$on('change', input);
+
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(input).toBeCalledWith('2012-05')
+ expect(change).not.toBeCalled()
+ })
+
+ it('should not emit input event on year click if month is not allowed', async () => {
+ const cb = jest.fn()
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05',
+ type: 'month',
+ allowedDates: () => false
+ },
+ data: {
+ activePicker: 'YEAR'
+ }
+ })
+
+ wrapper.vm.$on('input', cb);
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(cb).not.toBeCalled()
+ })
+
+ it('should emit input event on month click', async () => {
+ const cb = jest.fn()
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05',
+ type: 'month'
+ }
+ })
+
+ wrapper.vm.$on('input', cb);
+ wrapper.find('.date-picker-table--month button')[0].trigger('click')
+ expect(cb).toBeCalledWith('2013-01')
+ })
+
+ it('should be scrollable', async () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05',
+ type: 'month',
+ scrollable: true
+ }
+ })
+
+ wrapper.find('.date-picker-table--month')[0].trigger('wheel')
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.tableDate).toBe('2014')
+ })
+
+ it('should match snapshot with pick-month prop', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05-07',
+ type: 'month'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with allowed dates as array', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2013-05',
+ type: 'month',
+ allowedDates: value => ['2013-01', '2013-03', '2013-05', '2013-07'].includes(value)
+ }
+ })
+
+ expect(wrapper.find('.date-picker-table--month tbody')[0].html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with month formatting functions', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ type: 'month',
+ monthFormat: date => `(${date.split('-')[1]})`
+ }
+ })
+
+ expect(wrapper.find('.date-picker-table--month tbody')[0].html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with colored picker', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ type: 'month',
+ value: '2005-11-01',
+ color: 'primary',
+ headerColor: 'orange darken-1'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with colored picker', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ type: 'month',
+ value: '2005-11-01',
+ color: 'orange darken-1'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match change month when clicked on header arrow buttons', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11',
+ type: 'month'
+ }
+ })
+
+ const [leftButton, rightButton] = wrapper.find('.date-picker-header button')
+
+ leftButton.trigger('click')
+ expect(wrapper.vm.tableDate).toBe('2004')
+
+ rightButton.trigger('click')
+ expect(wrapper.vm.tableDate).toBe('2006')
+ })
+
+ it('should match change active picker when clicked on month button', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: '2005-11-01',
+ type: 'month'
+ }
+ })
+
+ const button = wrapper.find('.date-picker-header strong')[0]
+
+ button.trigger('click')
+ expect(wrapper.vm.activePicker).toBe('YEAR')
+ })
+
+ it('should select year', async () => {
+ const wrapper = mount(VDatePicker, {
+ data: {
+ activePicker: 'YEAR'
+ },
+ propsData: {
+ type: 'month',
+ value: '2005-11'
+ }
+ })
+
+ wrapper.find('.date-picker-years li.active + li')[0].trigger('click')
+ expect(wrapper.vm.activePicker).toBe('MONTH')
+ expect(wrapper.vm.tableDate).toBe('2004')
+ })
+
+ it('should set the table date when value has changed', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ value: null,
+ type: 'month'
+ }
+ })
+
+ wrapper.setProps({ value: '2005-11' })
+ expect(wrapper.vm.tableDate).toBe('2005')
+ })
+
+ it('should use prev and next icons', () => {
+ const wrapper = mount(VDatePicker, {
+ propsData: {
+ type: 'month',
+ prevIcon: 'block',
+ nextIcon: 'check'
+ }
+ })
+
+ const icons = wrapper.find('.date-picker-header .icon')
+ expect(icons[0].element.textContent).toBe('block')
+ expect(icons[1].element.textContent).toBe('check')
+ })
+})
diff --git a/src/components/VDatePicker/VDatePicker.spec.js b/src/components/VDatePicker/VDatePicker.spec.js
deleted file mode 100644
index 9ac1d0b0258e..000000000000
--- a/src/components/VDatePicker/VDatePicker.spec.js
+++ /dev/null
@@ -1,183 +0,0 @@
-import VDatePicker from '~components/VDatePicker'
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
-
-test('VDatePicker.js', ({ mount }) => {
- it('should display the correct date in title and header', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2005-11-01',
- }
- })
-
- const title = wrapper.find('.picker--date__title-date div')[0]
- const header = wrapper.find('.picker--date__header-selector-date strong')[0]
-
- expect(title.text()).toBe('Tue, Nov 1')
- expect(header.text()).toBe('November 2005')
- })
-
- it('should match snapshot with default settings', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with pick-month prop', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- type: 'month'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with dark theme', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- dark: true
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with allowed dates', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- allowedDates: { min: '2013-05-03', max: '2013-05-19' }
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with allowed dates and pick-month prop', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05',
- type: 'month',
- allowedDates: ['2013-01', '2013-03', '2013-05', '2013-07', '2013-09']
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with no title', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- noTitle: true
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with first day of week', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- firstDayOfWeek: 2
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- // TODO: This fails in different ways for multiple people
- // Avoriaz/Jsdom (?) doesn't fully support date formatting using locale
- // This should be tested in browser env
- it.skip('should match snapshot with locale', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2013-05-07',
- locale: 'fa-AF'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with title/header formatting functions', () => {
- const dateFormat = date => `(${date})`
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2005-11-01',
- headerDateFormat: dateFormat,
- titleDateFormat: dateFormat
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with month formatting functions', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2005-11-01',
- type: 'month',
- monthFormat: date => `(${date.split('-')[1]})`
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with colored date picker', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2005-11-01',
- color: 'primary',
- headerColor: 'orange darken-1'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with colored date picker', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- value: '2005-11-01',
- color: 'orange darken-1'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with colored month picker', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- type: 'month',
- value: '2005-11-01',
- color: 'primary',
- headerColor: 'orange darken-1'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-
- it('should match snapshot with colored month picker', () => {
- const wrapper = mount(VDatePicker, {
- propsData: {
- type: 'month',
- value: '2005-11-01',
- color: 'orange darken-1'
- }
- })
-
- expect(wrapper.html()).toMatchSnapshot()
- })
-})
diff --git a/src/components/VDatePicker/VDatePickerDateTable.js b/src/components/VDatePicker/VDatePickerDateTable.js
new file mode 100644
index 000000000000..50112d562483
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerDateTable.js
@@ -0,0 +1,127 @@
+// Mixins
+import Colorable from '../../mixins/colorable'
+import DatePickerTable from './mixins/date-picker-table'
+
+// Utils
+import { pad, createNativeLocaleFormatter, monthChange } from './util'
+import { createRange } from '../../util/helpers'
+
+export default {
+ name: 'v-date-picker-date-table',
+
+ mixins: [
+ Colorable,
+ DatePickerTable
+ ],
+
+ props: {
+ events: {
+ type: [Array, Object, Function],
+ default: () => null
+ },
+ eventColor: {
+ type: [String, Function, Object],
+ default: 'warning'
+ },
+ firstDayOfWeek: {
+ type: [String, Number],
+ default: 0
+ },
+ weekdayFormat: {
+ type: Function,
+ default: null
+ }
+ },
+
+ computed: {
+ formatter () {
+ return this.format || createNativeLocaleFormatter(this.locale, { day: 'numeric', timeZone: 'UTC' }, { start: 8, length: 2 })
+ },
+ weekdayFormatter () {
+ return this.weekdayFormat || createNativeLocaleFormatter(this.locale, { weekday: 'narrow', timeZone: 'UTC' })
+ },
+ weekDays () {
+ const first = parseInt(this.firstDayOfWeek, 10)
+
+ return this.weekdayFormatter
+ ? createRange(7).map(i => this.weekdayFormatter(`2017-01-${first + i + 15}`)) // 2017-01-15 is Sunday
+ : createRange(7).map(i => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][(i + first) % 7])
+ }
+ },
+
+ methods: {
+ calculateTableDate (delta) {
+ return monthChange(this.tableDate, Math.sign(delta || 1))
+ },
+ genTHead () {
+ const days = this.weekDays.map(day => this.$createElement('th', day))
+ return this.$createElement('thead', this.genTR(days))
+ },
+ genEvent (date) {
+ let eventColor
+ if (typeof this.eventColor === 'string') {
+ eventColor = this.eventColor
+ } else if (typeof this.eventColor === 'function') {
+ eventColor = this.eventColor(date)
+ } else {
+ eventColor = this.eventColor[date]
+ }
+ return this.$createElement('div', {
+ staticClass: 'date-picker-table__event',
+ class: this.addBackgroundColorClassChecks({}, eventColor || this.color)
+ })
+ },
+ // Returns number of the days from the firstDayOfWeek to the first day of the current month
+ weekDaysBeforeFirstDayOfTheMonth () {
+ const firstDayOfTheMonth = new Date(`${this.displayedYear}-${pad(this.displayedMonth + 1)}-01T00:00:00+00:00`)
+ const weekDay = firstDayOfTheMonth.getUTCDay()
+ return (weekDay - parseInt(this.firstDayOfWeek) + 7) % 7
+ },
+ isEvent (date) {
+ if (Array.isArray(this.events)) {
+ return this.events.indexOf(date) > -1
+ } else if (this.events instanceof Function) {
+ return this.events(date)
+ } else {
+ return false
+ }
+ },
+ genTBody () {
+ const children = []
+ const daysInMonth = new Date(this.displayedYear, this.displayedMonth + 1, 0).getDate()
+ let rows = []
+ let day = this.weekDaysBeforeFirstDayOfTheMonth()
+
+ while (day--) rows.push(this.$createElement('td'))
+ for (day = 1; day <= daysInMonth; day++) {
+ const date = `${this.displayedYear}-${pad(this.displayedMonth + 1)}-${pad(day)}`
+
+ rows.push(this.$createElement('td', [
+ this.genButton(date, true),
+ this.isEvent(date) ? this.genEvent(date) : null
+ ]))
+
+ if (rows.length % 7 === 0) {
+ children.push(this.genTR(rows))
+ rows = []
+ }
+ }
+
+ if (rows.length) {
+ children.push(this.genTR(rows))
+ }
+
+ return this.$createElement('tbody', children)
+ },
+ genTR (children) {
+ return [this.$createElement('tr', children)]
+ }
+ },
+
+ render (h) {
+ return this.genTable('date-picker-table date-picker-table--date', [
+ this.genTHead(),
+ this.genTBody()
+ ])
+ }
+}
diff --git a/src/components/VDatePicker/VDatePickerDateTable.spec.js b/src/components/VDatePicker/VDatePickerDateTable.spec.js
new file mode 100644
index 000000000000..edaf207a301f
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerDateTable.spec.js
@@ -0,0 +1,212 @@
+import { compileToFunctions } from 'vue-template-compiler'
+import VDatePickerDateTable from './VDatePickerDateTable'
+import { test } from '@util/testing'
+
+test('VDatePickerDateTable.js', ({ mount }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with events (array) and match snapshot', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ events: ['2005-05-03'],
+ eventColor: 'red'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with events (function) and match snapshot', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ events: date => date === '2005-05-03',
+ eventColor: 'red'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with events colored by object and match snapshot', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ events: ['2005-05-03', '2005-05-04'],
+ eventColor: {'2005-05-03': 'red', '2005-05-04': 'blue lighten-1'}
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with events colored by function and match snapshot', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ events: ['2005-05-03', '2005-05-04'],
+ eventColor: date => ({'2005-05-03': 'red'}[date])
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with first day of week', function () {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ firstDayOfWeek: 2
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should watch tableDate value and run transition', async () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03'
+ }
+ })
+
+ wrapper.setProps({
+ tableDate: '2005-06'
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('table')[0].element.className).toBe('tab-transition-enter tab-transition-enter-active')
+ })
+
+ it('should watch tableDate value and run reverse transition', async () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03'
+ }
+ })
+
+ wrapper.setProps({
+ tableDate: '2005-04'
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('table')[0].element.className).toBe('tab-reverse-transition-enter tab-reverse-transition-enter-active')
+ })
+
+ it('should emit event when date button is clicked', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('tbody button')[0].trigger('click')
+ expect(input).toBeCalledWith('2005-05-01')
+ })
+
+ it('should not emit event when disabled month button is clicked', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ current: '2005-07',
+ value: '2005-11-03',
+ allowedDates: () => false
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('tbody button')[0].trigger('click')
+ expect(input).not.toBeCalled()
+ })
+
+ it('should emit tableDate event when scrolled and scrollable', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05',
+ scrollable: true
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('wheel')
+ expect(tableDate).toBeCalledWith('2005-06')
+ })
+
+ it('should not emit tableDate event when scrolled and not scrollable', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('wheel')
+ expect(tableDate).not.toBeCalled()
+ })
+
+ // TODO
+ it.skip('should emit tableDate event when swiped', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('touchstart')
+ wrapper.trigger('touchend')
+ expect(tableDate).toBeCalledWith('2005-06')
+ })
+
+ it('should change tableDate when touch is called', () => {
+ const wrapper = mount(VDatePickerDateTable, {
+ propsData: {
+ tableDate: '2005-05'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.vm.touch(1)
+ expect(tableDate).toBeCalledWith('2005-06')
+ wrapper.vm.touch(-1)
+ expect(tableDate).toBeCalledWith('2005-04')
+ })
+})
diff --git a/src/components/VDatePicker/VDatePickerHeader.js b/src/components/VDatePicker/VDatePickerHeader.js
new file mode 100644
index 000000000000..e1e0f88f06f0
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerHeader.js
@@ -0,0 +1,138 @@
+import '../../stylus/components/_date-picker-header.styl'
+
+// Components
+import VBtn from '../VBtn'
+import VIcon from '../VIcon'
+
+// Mixins
+import Colorable from '../../mixins/colorable'
+
+// Utils
+import { createNativeLocaleFormatter, monthChange } from './util'
+
+export default {
+ name: 'v-date-picker-header',
+
+ components: {
+ VBtn,
+ VIcon
+ },
+
+ mixins: [Colorable],
+
+ data () {
+ return {
+ isReversing: false,
+ defaultColor: 'accent'
+ }
+ },
+
+ props: {
+ disabled: Boolean,
+ format: {
+ type: Function,
+ default: null
+ },
+ locale: {
+ type: String,
+ default: 'en-us'
+ },
+ min: String,
+ max: String,
+ nextIcon: {
+ type: String,
+ default: 'chevron_right'
+ },
+ prevIcon: {
+ type: String,
+ default: 'chevron_left'
+ },
+ value: {
+ type: [Number, String],
+ required: true
+ }
+ },
+
+ computed: {
+ formatter () {
+ if (this.format) {
+ return this.format
+ } else if (String(this.value).split('-')[1]) {
+ return createNativeLocaleFormatter(this.locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }, { length: 7 })
+ } else {
+ return createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 })
+ }
+ }
+ },
+
+ watch: {
+ value (newVal, oldVal) {
+ this.isReversing = newVal < oldVal
+ }
+ },
+
+ methods: {
+ genBtn (change) {
+ const disabled = this.disabled ||
+ (change < 0 && this.min && this.calculateChange(change) < this.min) ||
+ (change > 0 && this.max && this.calculateChange(change) > this.max)
+
+ return this.$createElement('v-btn', {
+ props: {
+ dark: this.dark,
+ disabled,
+ icon: true
+ },
+ nativeOn: {
+ click: e => {
+ e.stopPropagation()
+ this.$emit('input', this.calculateChange(change))
+ }
+ }
+ }, [
+ this.$createElement('v-icon', change < 0 ? this.prevIcon : this.nextIcon)
+ ])
+ },
+ calculateChange (sign) {
+ const [year, month] = String(this.value).split('-').map(v => 1 * v)
+
+ if (month == null) {
+ return `${year + sign}`
+ } else {
+ return monthChange(String(this.value), sign)
+ }
+ },
+ genHeader () {
+ const header = this.$createElement('strong', {
+ 'class': this.disabled ? undefined : this.addTextColorClassChecks(),
+ key: String(this.value),
+ on: {
+ click: () => this.$emit('toggle')
+ }
+ }, [this.$slots.default || this.formatter(String(this.value))])
+
+ const transition = this.$createElement('transition', {
+ props: {
+ name: this.isReversing ? 'tab-reverse-transition' : 'tab-transition'
+ }
+ }, [header])
+
+ return this.$createElement('div', {
+ staticClass: 'date-picker-header__value',
+ class: {
+ 'date-picker-header__value--disabled': this.disabled
+ }
+ }, [transition])
+ }
+ },
+
+ render (h) {
+ return this.$createElement('div', {
+ staticClass: 'date-picker-header'
+ }, [
+ this.genBtn(-1),
+ this.genHeader(),
+ this.genBtn(+1)
+ ])
+ }
+}
diff --git a/src/components/VDatePicker/VDatePickerHeader.spec.js b/src/components/VDatePicker/VDatePickerHeader.spec.js
new file mode 100644
index 000000000000..c78e8d6ca60a
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerHeader.spec.js
@@ -0,0 +1,153 @@
+import { compileToFunctions } from 'vue-template-compiler'
+import VDatePickerHeader from './VDatePickerHeader'
+import { test } from '@util/testing'
+
+test('VDatePickerHeader.js', ({ mount }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-11'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with year value and match snapshot', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005'
+ }
+ })
+
+ expect(wrapper.find('.date-picker-header__value strong')[0].element.textContent).toBe('2005')
+ })
+
+ it('should render prev/next icons', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005',
+ prevIcon: 'foo',
+ nextIcon: 'bar'
+ }
+ })
+
+ expect(wrapper.find('.icon')[0].element.textContent).toBe('foo')
+ expect(wrapper.find('.icon')[1].element.textContent).toBe('bar')
+ })
+
+ it('should render component with own formatter and match snapshot', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-11',
+ format: value => `(${value})`
+ }
+ })
+
+ expect(wrapper.find('.date-picker-header__value strong')[0].element.textContent).toBe('(2005-11)')
+ })
+
+ it('should render colored component and match snapshot', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-11',
+ color: 'green lighten-1'
+ }
+ })
+
+ const strong = wrapper.find('.date-picker-header__value strong')[0]
+ expect(strong.hasClass('green--text')).toBe(true)
+ expect(strong.hasClass('text--lighten-1')).toBe(true)
+ })
+
+ it('should render component with default slot and match snapshot', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-11'
+ },
+ slots: {
+ default: [compileToFunctions('foo ')]
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should trigger event on selector click', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-11'
+ }
+ })
+
+ const toggle = jest.fn()
+ wrapper.vm.$on('toggle', toggle)
+
+ wrapper.find('.date-picker-header__value strong')[0].trigger('click')
+ expect(toggle).toBeCalled()
+ })
+
+ it('should trigger event on arrows click', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-12'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('button')[0].trigger('click')
+ expect(input).toBeCalledWith('2005-11')
+
+ wrapper.find('button')[1].trigger('click')
+ expect(input).toBeCalledWith('2006-01')
+ })
+
+ it('should calculate prev/next value', () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: '2005-12'
+ }
+ })
+ expect(wrapper.vm.calculateChange(-1)).toBe('2005-11')
+ expect(wrapper.vm.calculateChange(+1)).toBe('2006-01')
+
+ wrapper.setProps({
+ value: '2005'
+ })
+ expect(wrapper.vm.calculateChange(-1)).toBe('2004')
+ expect(wrapper.vm.calculateChange(+1)).toBe('2006')
+ })
+
+ it('should watch value and run transition', async () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: 2005
+ }
+ })
+
+ wrapper.setProps({
+ value: 2006
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('.date-picker-header__value strong')[0].hasClass('tab-transition-enter')).toBe(true)
+ expect(wrapper.find('.date-picker-header__value strong')[0].hasClass('tab-transition-enter-active')).toBe(true)
+ })
+
+ it('should watch value and run reverse transition', async () => {
+ const wrapper = mount(VDatePickerHeader, {
+ propsData: {
+ value: 2005
+ }
+ })
+
+ wrapper.setProps({
+ value: 2004
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('.date-picker-header__value strong')[0].hasClass('tab-reverse-transition-enter')).toBe(true)
+ expect(wrapper.find('.date-picker-header__value strong')[0].hasClass('tab-reverse-transition-enter-active')).toBe(true)
+ })
+
+})
diff --git a/src/components/VDatePicker/VDatePickerMonthTable.js b/src/components/VDatePicker/VDatePickerMonthTable.js
new file mode 100644
index 000000000000..8b3eb75ec183
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerMonthTable.js
@@ -0,0 +1,55 @@
+// Mixins
+import Colorable from '../../mixins/colorable'
+import DatePickerTable from './mixins/date-picker-table'
+
+// Utils
+import { pad, createNativeLocaleFormatter } from './util'
+
+export default {
+ name: 'v-date-picker-month-table',
+
+ mixins: [
+ Colorable,
+ DatePickerTable
+ ],
+
+ computed: {
+ formatter () {
+ return this.format || createNativeLocaleFormatter(this.locale, { month: 'short', timeZone: 'UTC' }, { start: 5, length: 2 })
+ }
+ },
+
+ methods: {
+ calculateTableDate (delta) {
+ return `${parseInt(this.tableDate, 10) + Math.sign(delta || 1)}`
+ },
+ genTBody () {
+ const children = []
+ const cols = Array(3).fill(null)
+ const rows = 12 / cols.length
+
+ for (let row = 0; row < rows; row++) {
+ const tds = cols.map((_, col) => {
+ const month = row * cols.length + col
+ return this.$createElement('td', {
+ key: month
+ }, [
+ this.genButton(`${this.displayedYear}-${pad(month + 1)}`, false)
+ ])
+ })
+
+ children.push(this.$createElement('tr', {
+ key: row
+ }, tds))
+ }
+
+ return this.$createElement('tbody', children)
+ }
+ },
+
+ render (h) {
+ return this.genTable('date-picker-table date-picker-table--month', [
+ this.genTBody()
+ ])
+ }
+}
diff --git a/src/components/VDatePicker/VDatePickerMonthTable.spec.js b/src/components/VDatePicker/VDatePickerMonthTable.spec.js
new file mode 100644
index 000000000000..134807cd5cf0
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerMonthTable.spec.js
@@ -0,0 +1,143 @@
+import { compileToFunctions } from 'vue-template-compiler'
+import VDatePickerMonthTable from './VDatePickerMonthTable'
+import { test } from '@util/testing'
+
+test('VDatePickerMonthTable.js', ({ mount }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ current: '2005-05',
+ value: '2005-11'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should watch tableDate value and run transition', async () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ current: '2005-05',
+ value: '2005-11'
+ }
+ })
+
+ wrapper.setProps({
+ tableDate: '2006'
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('table')[0].element.className).toBe('tab-transition-enter tab-transition-enter-active')
+ })
+
+ it('should watch tableDate value and run reverse transition', async () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ current: '2005-05',
+ value: '2005-11'
+ }
+ })
+
+ wrapper.setProps({
+ tableDate: '2004'
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.find('table')[0].element.className).toBe('tab-reverse-transition-enter tab-reverse-transition-enter-active')
+ })
+
+ it('should emit event when month button is clicked', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ current: '2005-05',
+ value: '2005-11'
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('tbody button')[0].trigger('click')
+ expect(input).toBeCalledWith('2005-01')
+ })
+
+ it('should not emit event when disabled month button is clicked', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ current: '2005-05',
+ value: '2005-11',
+ allowedDates: () => false
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('tbody button')[0].trigger('click')
+ expect(input).not.toBeCalled()
+ })
+
+ it('should emit tableDate event when scrolled and scrollable', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005',
+ scrollable: true
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('wheel')
+ expect(tableDate).toBeCalledWith('2006')
+ })
+
+ it('should not emit tableDate event when scrolled and not scrollable', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('wheel')
+ expect(tableDate).not.toBeCalled()
+ })
+
+ // TODO
+ it.skip('should emit tableDate event when swiped', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.trigger('touchstart')
+ wrapper.trigger('touchend')
+ expect(tableDate).toBeCalledWith(2006)
+ })
+
+ it('should change tableDate when touch is called', () => {
+ const wrapper = mount(VDatePickerMonthTable, {
+ propsData: {
+ tableDate: '2005'
+ }
+ })
+
+ const tableDate = jest.fn()
+ wrapper.vm.$on('tableDate', tableDate)
+
+ wrapper.vm.touch(1)
+ expect(tableDate).toBeCalledWith('2006')
+ wrapper.vm.touch(-1)
+ expect(tableDate).toBeCalledWith('2004')
+ })
+})
diff --git a/src/components/VDatePicker/VDatePickerTitle.js b/src/components/VDatePicker/VDatePickerTitle.js
new file mode 100644
index 000000000000..0cb98892f1ac
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerTitle.js
@@ -0,0 +1,91 @@
+import '../../stylus/components/_date-picker-title.styl'
+
+// Components
+import VIcon from '../VIcon'
+
+// Mixins
+import PickerButton from '../../mixins/picker-button'
+
+export default {
+ name: 'v-date-picker-title',
+
+ components: {
+ VIcon
+ },
+
+ mixins: [PickerButton],
+
+ data: () => ({
+ isReversing: false
+ }),
+
+ props: {
+ date: {
+ type: String,
+ default: ''
+ },
+ selectingYear: Boolean,
+ year: {
+ type: [Number, String],
+ default: ''
+ },
+ yearIcon: {
+ type: String
+ },
+ value: {
+ type: String
+ }
+ },
+
+ computed: {
+ computedTransition () {
+ return this.isReversing ? 'picker-reverse-transition' : 'picker-transition'
+ }
+ },
+
+ watch: {
+ value (val, prev) {
+ this.isReversing = val < prev
+ }
+ },
+
+ methods: {
+ genYearIcon () {
+ return this.$createElement('v-icon', {
+ props: {
+ dark: true
+ }
+ }, this.yearIcon)
+ },
+ getYearBtn () {
+ return this.genPickerButton('selectingYear', true, [
+ this.year,
+ this.yearIcon ? this.genYearIcon() : null
+ ], 'date-picker-title__year')
+ },
+ genTitleText () {
+ return this.$createElement('transition', {
+ props: {
+ name: this.computedTransition
+ }
+ }, [
+ this.$createElement('div', {
+ domProps: { innerHTML: this.date || ' ' },
+ key: this.value
+ })
+ ])
+ },
+ genTitleDate (title) {
+ return this.genPickerButton('selectingYear', false, this.genTitleText(title), 'date-picker-title__date')
+ }
+ },
+
+ render (h) {
+ return h('div', {
+ staticClass: 'date-picker-title'
+ }, [
+ this.getYearBtn(),
+ this.genTitleDate()
+ ])
+ }
+}
diff --git a/src/components/VDatePicker/VDatePickerTitle.spec.js b/src/components/VDatePicker/VDatePickerTitle.spec.js
new file mode 100644
index 000000000000..07861b388a38
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerTitle.spec.js
@@ -0,0 +1,88 @@
+import VDatePickerTitle from './VDatePickerTitle'
+import { test } from '@util/testing'
+
+test('VDatePickerTitle.js', ({ mount }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VDatePickerTitle, {
+ propsData: {
+ year: '1234',
+ date: '2005-11-01'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component when selecting year and match snapshot', () => {
+ const wrapper = mount(VDatePickerTitle, {
+ propsData: {
+ year: '1234',
+ date: '2005-11-01',
+ selectingYear: true
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render year icon', () => {
+ const wrapper = mount(VDatePickerTitle, {
+ propsData: {
+ year: '1234',
+ yearIcon: 'year',
+ date: '2005-11-01'
+ }
+ })
+
+ expect(wrapper.find('.date-picker-title__year')[0].html()).toMatchSnapshot()
+ })
+
+ it('should emit input event on year/date click', () => {
+ const wrapper = mount(VDatePickerTitle, {
+ propsData: {
+ year: '1234',
+ yearIcon: 'year',
+ date: '2005-11-01'
+ }
+ })
+
+ const input = jest.fn(value => wrapper.setProps({ selectingYear: value }))
+ wrapper.vm.$on('update:selectingYear', input)
+
+ wrapper.find('.date-picker-title__date')[0].trigger('click')
+ expect(input).not.toBeCalled()
+ wrapper.find('.date-picker-title__year')[0].trigger('click')
+ expect(input).toBeCalledWith(true)
+ wrapper.find('.date-picker-title__date')[0].trigger('click')
+ expect(input).toBeCalledWith(false)
+ wrapper.find('.date-picker-title__year')[0].trigger('click')
+ wrapper.find('.date-picker-title__year')[0].trigger('click')
+ expect(input).toBeCalledWith(false)
+ })
+
+ it('should have the correct transition', () => {
+ const wrapper = mount(VDatePickerTitle, {
+ propsData: {
+ year: '2018',
+ date: 'Tue, Mar 3',
+ value: '2018-03-03'
+ }
+ })
+
+ expect(wrapper.vm.isReversing).toBe(false)
+
+ wrapper.setProps({
+ date: 'Wed, Mar 4',
+ value: '2018-03-04'
+ })
+
+ expect(wrapper.vm.isReversing).toBe(false)
+
+ wrapper.setProps({
+ date: 'Wed, Mar 3',
+ value: '2018-03-03'
+ })
+
+ expect(wrapper.vm.isReversing).toBe(true)
+ })
+})
diff --git a/src/components/VDatePicker/VDatePickerYears.js b/src/components/VDatePicker/VDatePickerYears.js
new file mode 100644
index 000000000000..8c068928c1b1
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerYears.js
@@ -0,0 +1,78 @@
+import '../../stylus/components/_date-picker-years.styl'
+
+// Mixins
+import Colorable from '../../mixins/colorable'
+
+// Utils
+import { createNativeLocaleFormatter } from './util'
+
+export default {
+ name: 'v-date-picker-years',
+
+ mixins: [Colorable],
+
+ data () {
+ return {
+ defaultColor: 'primary'
+ }
+ },
+
+ props: {
+ format: {
+ type: Function,
+ default: null
+ },
+ locale: {
+ type: String,
+ default: 'en-us'
+ },
+ min: [Number, String],
+ max: [Number, String],
+ value: [Number, String]
+ },
+
+ computed: {
+ formatter () {
+ return this.format || createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 })
+ }
+ },
+
+ mounted () {
+ this.$el.scrollTop = this.$el.scrollHeight / 2 - this.$el.offsetHeight / 2
+ },
+
+ methods: {
+ genYearItem (year) {
+ const formatted = this.formatter(`${year}`)
+
+ return this.$createElement('li', {
+ key: year,
+ 'class': parseInt(this.value, 10) === year
+ ? this.addTextColorClassChecks({ active: true })
+ : {},
+ on: {
+ click: () => this.$emit('input', year)
+ }
+ }, formatted)
+ },
+ genYearItems () {
+ const children = []
+ const selectedYear = this.value ? parseInt(this.value, 10) : new Date().getFullYear()
+ const maxYear = this.max ? parseInt(this.max, 10) : (selectedYear + 100)
+ const minYear = Math.min(maxYear, this.min ? parseInt(this.min, 10) : (selectedYear - 100))
+
+ for (let year = maxYear; year >= minYear; year--) {
+ children.push(this.genYearItem(year))
+ }
+
+ return children
+ }
+ },
+
+ render (h) {
+ return this.$createElement('ul', {
+ staticClass: 'date-picker-years',
+ ref: 'years'
+ }, this.genYearItems())
+ }
+}
diff --git a/src/components/VDatePicker/VDatePickerYears.spec.js b/src/components/VDatePicker/VDatePickerYears.spec.js
new file mode 100644
index 000000000000..2f6e42e83a84
--- /dev/null
+++ b/src/components/VDatePicker/VDatePickerYears.spec.js
@@ -0,0 +1,64 @@
+import VDatePickerYears from './VDatePickerYears'
+import { test } from '@util/testing'
+
+test('VDatePickerYears.js', ({ mount }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VDatePickerYears, {
+ propsData: {
+ value: '2000'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should respect min/max props', async () => {
+ const wrapper = mount(VDatePickerYears, {
+ propsData: {
+ min: 1234,
+ max: 1238
+ }
+ })
+
+ expect(wrapper.find('li:first-child')[0].element.textContent).toBe('1238')
+ expect(wrapper.find('li:last-child')[0].element.textContent).toBe('1234')
+ })
+
+ it('should not allow min to be greater then max', async () => {
+ const wrapper = mount(VDatePickerYears, {
+ propsData: {
+ min: 1238,
+ max: 1234
+ }
+ })
+ expect(wrapper.find('li').length).toBe(1)
+ expect(wrapper.find('li')[0].element.textContent).toBe('1234')
+ expect(wrapper.find('li')[0].element.textContent).toBe('1234')
+ })
+
+ it('should emit event on year click', async () => {
+ const wrapper = mount(VDatePickerYears, {
+ propsData: {
+ value: 1999
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ wrapper.find('li.active + li')[0].trigger('click')
+ expect(input).toBeCalledWith(1998)
+ })
+
+ it('should format years', async () => {
+ const wrapper = mount(VDatePickerYears, {
+ propsData: {
+ format: year => `(${year})`,
+ min: 1001,
+ max: 1001
+ }
+ })
+
+ expect(wrapper.find('li')[0].element.textContent).toBe('(1001)')
+ })
+})
diff --git a/src/components/VDatePicker/__snapshots__/VDatePicker.date.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePicker.date.spec.js.snap
new file mode 100755
index 000000000000..1389c1cd0e25
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePicker.date.spec.js.snap
@@ -0,0 +1,3448 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePicker.js should match snapshot with colored picker 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with colored picker 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with dark theme 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with default settings 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with year icon 1`] = `
+
+
+
+
+ 2005
+
+ year
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should render component with min/max props 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should render component with min/max props 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should render component with min/max props 3`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should render readonly picker 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePicker.month.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePicker.month.spec.js.snap
new file mode 100755
index 000000000000..8a1615aed20b
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePicker.month.spec.js.snap
@@ -0,0 +1,789 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePicker.js should match snapshot with allowed dates as array 1`] = `
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with colored picker 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with colored picker 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with month formatting functions 1`] = `
+
+
+
+
+
+
+ (01)
+
+
+
+
+
+
+ (02)
+
+
+
+
+
+
+ (03)
+
+
+
+
+
+
+
+
+ (04)
+
+
+
+
+
+
+ (05)
+
+
+
+
+
+
+ (06)
+
+
+
+
+
+
+
+
+ (07)
+
+
+
+
+
+
+ (08)
+
+
+
+
+
+
+ (09)
+
+
+
+
+
+
+
+
+ (10)
+
+
+
+
+
+
+ (11)
+
+
+
+
+
+
+ (12)
+
+
+
+
+
+
+`;
+
+exports[`VDatePicker.js should match snapshot with pick-month prop 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePicker.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePicker.spec.js.snap
deleted file mode 100644
index c52e062b77cb..000000000000
--- a/src/components/VDatePicker/__snapshots__/VDatePicker.spec.js.snap
+++ /dev/null
@@ -1,4306 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`VDatePicker.js should match snapshot with allowed dates 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with allowed dates and pick-month prop 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Jan
-
-
-
-
-
-
- Feb
-
-
-
-
-
-
- Mar
-
-
-
-
-
-
-
-
- Apr
-
-
-
-
-
-
- May
-
-
-
-
-
-
- Jun
-
-
-
-
-
-
-
-
- Jul
-
-
-
-
-
-
- Aug
-
-
-
-
-
-
- Sep
-
-
-
-
-
-
-
-
- Oct
-
-
-
-
-
-
- Nov
-
-
-
-
-
-
- Dec
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with colored date picker 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
- 12
-
-
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
- 19
-
-
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
- 26
-
-
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with colored date picker 2`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
- 12
-
-
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
- 19
-
-
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
- 26
-
-
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with colored month picker 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Jan
-
-
-
-
-
-
- Feb
-
-
-
-
-
-
- Mar
-
-
-
-
-
-
-
-
- Apr
-
-
-
-
-
-
- May
-
-
-
-
-
-
- Jun
-
-
-
-
-
-
-
-
- Jul
-
-
-
-
-
-
- Aug
-
-
-
-
-
-
- Sep
-
-
-
-
-
-
-
-
- Oct
-
-
-
-
-
-
- Nov
-
-
-
-
-
-
- Dec
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with colored month picker 2`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Jan
-
-
-
-
-
-
- Feb
-
-
-
-
-
-
- Mar
-
-
-
-
-
-
-
-
- Apr
-
-
-
-
-
-
- May
-
-
-
-
-
-
- Jun
-
-
-
-
-
-
-
-
- Jul
-
-
-
-
-
-
- Aug
-
-
-
-
-
-
- Sep
-
-
-
-
-
-
-
-
- Oct
-
-
-
-
-
-
- Nov
-
-
-
-
-
-
- Dec
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with dark theme 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with default settings 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with first day of week 1`] = `
-
-
-
-
-
-
-
-
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
- S
-
-
- M
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with locale 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with month formatting functions 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- (01)
-
-
-
-
-
-
- (02)
-
-
-
-
-
-
- (03)
-
-
-
-
-
-
-
-
- (04)
-
-
-
-
-
-
- (05)
-
-
-
-
-
-
- (06)
-
-
-
-
-
-
-
-
- (07)
-
-
-
-
-
-
- (08)
-
-
-
-
-
-
- (09)
-
-
-
-
-
-
-
-
- (10)
-
-
-
-
-
-
- (11)
-
-
-
-
-
-
- (12)
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with no title 1`] = `
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
-
-
- 12
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
-
-
- 19
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
-
-
- 26
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
- 31
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with pick-month prop 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Jan
-
-
-
-
-
-
- Feb
-
-
-
-
-
-
- Mar
-
-
-
-
-
-
-
-
- Apr
-
-
-
-
-
-
- May
-
-
-
-
-
-
- Jun
-
-
-
-
-
-
-
-
- Jul
-
-
-
-
-
-
- Aug
-
-
-
-
-
-
- Sep
-
-
-
-
-
-
-
-
- Oct
-
-
-
-
-
-
- Nov
-
-
-
-
-
-
- Dec
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`VDatePicker.js should match snapshot with title/header formatting functions 1`] = `
-
-
-
-
-
-
-
-
-
-
- S
-
-
- M
-
-
- T
-
-
- W
-
-
- T
-
-
- F
-
-
- S
-
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- 3
-
-
-
-
-
-
- 4
-
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
- 6
-
-
-
-
-
-
- 7
-
-
-
-
-
-
- 8
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 11
-
-
-
-
-
-
- 12
-
-
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 14
-
-
-
-
-
-
- 15
-
-
-
-
-
-
- 16
-
-
-
-
-
-
- 17
-
-
-
-
-
-
- 18
-
-
-
-
-
-
- 19
-
-
-
-
-
-
-
-
- 20
-
-
-
-
-
-
- 21
-
-
-
-
-
-
- 22
-
-
-
-
-
-
- 23
-
-
-
-
-
-
- 24
-
-
-
-
-
-
- 25
-
-
-
-
-
-
- 26
-
-
-
-
-
-
-
-
- 27
-
-
-
-
-
-
- 28
-
-
-
-
-
-
- 29
-
-
-
-
-
-
- 30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePickerDateTable.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePickerDateTable.spec.js.snap
new file mode 100644
index 000000000000..7594cd157eb2
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePickerDateTable.spec.js.snap
@@ -0,0 +1,1975 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePickerDateTable.js should match snapshot with first day of week 1`] = `
+
+
+
+
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+ S
+
+
+ M
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePickerDateTable.js should render component and match snapshot 1`] = `
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePickerDateTable.js should render component with events (array) and match snapshot 1`] = `
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePickerDateTable.js should render component with events (function) and match snapshot 1`] = `
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePickerDateTable.js should render component with events colored by function and match snapshot 1`] = `
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
+
+exports[`VDatePickerDateTable.js should render component with events colored by object and match snapshot 1`] = `
+
+
+
+
+
+
+ S
+
+
+ M
+
+
+ T
+
+
+ W
+
+
+ T
+
+
+ F
+
+
+ S
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+ 9
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 16
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 21
+
+
+
+
+
+
+
+
+ 22
+
+
+
+
+
+
+ 23
+
+
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+
+
+ 28
+
+
+
+
+
+
+
+
+ 29
+
+
+
+
+
+
+ 30
+
+
+
+
+
+
+ 31
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePickerHeader.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePickerHeader.spec.js.snap
new file mode 100644
index 000000000000..d8d3ee55b983
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePickerHeader.spec.js.snap
@@ -0,0 +1,71 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePickerHeader.js should render component and match snapshot 1`] = `
+
+
+
+`;
+
+exports[`VDatePickerHeader.js should render component with default slot and match snapshot 1`] = `
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePickerMonthTable.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePickerMonthTable.spec.js.snap
new file mode 100644
index 000000000000..077bd23b9c7a
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePickerMonthTable.spec.js.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePickerMonthTable.js should render component and match snapshot 1`] = `
+
+
+
+
+
+
+
+
+ Jan
+
+
+
+
+
+
+ Feb
+
+
+
+
+
+
+ Mar
+
+
+
+
+
+
+
+
+ Apr
+
+
+
+
+
+
+ May
+
+
+
+
+
+
+ Jun
+
+
+
+
+
+
+
+
+ Jul
+
+
+
+
+
+
+ Aug
+
+
+
+
+
+
+ Sep
+
+
+
+
+
+
+
+
+ Oct
+
+
+
+
+
+
+ Nov
+
+
+
+
+
+
+ Dec
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePickerTitle.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePickerTitle.spec.js.snap
new file mode 100644
index 000000000000..bce5c0d3106a
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePickerTitle.spec.js.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePickerTitle.js should render component and match snapshot 1`] = `
+
+
+
+`;
+
+exports[`VDatePickerTitle.js should render component when selecting year and match snapshot 1`] = `
+
+
+
+`;
+
+exports[`VDatePickerTitle.js should render year icon 1`] = `
+
+
+ 1234
+
+ year
+
+
+
+`;
diff --git a/src/components/VDatePicker/__snapshots__/VDatePickerYears.spec.js.snap b/src/components/VDatePicker/__snapshots__/VDatePickerYears.spec.js.snap
new file mode 100644
index 000000000000..0a168c1934bd
--- /dev/null
+++ b/src/components/VDatePicker/__snapshots__/VDatePickerYears.spec.js.snap
@@ -0,0 +1,611 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VDatePickerYears.js should render component and match snapshot 1`] = `
+
+
+
+ 2100
+
+
+ 2099
+
+
+ 2098
+
+
+ 2097
+
+
+ 2096
+
+
+ 2095
+
+
+ 2094
+
+
+ 2093
+
+
+ 2092
+
+
+ 2091
+
+
+ 2090
+
+
+ 2089
+
+
+ 2088
+
+
+ 2087
+
+
+ 2086
+
+
+ 2085
+
+
+ 2084
+
+
+ 2083
+
+
+ 2082
+
+
+ 2081
+
+
+ 2080
+
+
+ 2079
+
+
+ 2078
+
+
+ 2077
+
+
+ 2076
+
+
+ 2075
+
+
+ 2074
+
+
+ 2073
+
+
+ 2072
+
+
+ 2071
+
+
+ 2070
+
+
+ 2069
+
+
+ 2068
+
+
+ 2067
+
+
+ 2066
+
+
+ 2065
+
+
+ 2064
+
+
+ 2063
+
+
+ 2062
+
+
+ 2061
+
+
+ 2060
+
+
+ 2059
+
+
+ 2058
+
+
+ 2057
+
+
+ 2056
+
+
+ 2055
+
+
+ 2054
+
+
+ 2053
+
+
+ 2052
+
+
+ 2051
+
+
+ 2050
+
+
+ 2049
+
+
+ 2048
+
+
+ 2047
+
+
+ 2046
+
+
+ 2045
+
+
+ 2044
+
+
+ 2043
+
+
+ 2042
+
+
+ 2041
+
+
+ 2040
+
+
+ 2039
+
+
+ 2038
+
+
+ 2037
+
+
+ 2036
+
+
+ 2035
+
+
+ 2034
+
+
+ 2033
+
+
+ 2032
+
+
+ 2031
+
+
+ 2030
+
+
+ 2029
+
+
+ 2028
+
+
+ 2027
+
+
+ 2026
+
+
+ 2025
+
+
+ 2024
+
+
+ 2023
+
+
+ 2022
+
+
+ 2021
+
+
+ 2020
+
+
+ 2019
+
+
+ 2018
+
+
+ 2017
+
+
+ 2016
+
+
+ 2015
+
+
+ 2014
+
+
+ 2013
+
+
+ 2012
+
+
+ 2011
+
+
+ 2010
+
+
+ 2009
+
+
+ 2008
+
+
+ 2007
+
+
+ 2006
+
+
+ 2005
+
+
+ 2004
+
+
+ 2003
+
+
+ 2002
+
+
+ 2001
+
+
+ 2000
+
+
+ 1999
+
+
+ 1998
+
+
+ 1997
+
+
+ 1996
+
+
+ 1995
+
+
+ 1994
+
+
+ 1993
+
+
+ 1992
+
+
+ 1991
+
+
+ 1990
+
+
+ 1989
+
+
+ 1988
+
+
+ 1987
+
+
+ 1986
+
+
+ 1985
+
+
+ 1984
+
+
+ 1983
+
+
+ 1982
+
+
+ 1981
+
+
+ 1980
+
+
+ 1979
+
+
+ 1978
+
+
+ 1977
+
+
+ 1976
+
+
+ 1975
+
+
+ 1974
+
+
+ 1973
+
+
+ 1972
+
+
+ 1971
+
+
+ 1970
+
+
+ 1969
+
+
+ 1968
+
+
+ 1967
+
+
+ 1966
+
+
+ 1965
+
+
+ 1964
+
+
+ 1963
+
+
+ 1962
+
+
+ 1961
+
+
+ 1960
+
+
+ 1959
+
+
+ 1958
+
+
+ 1957
+
+
+ 1956
+
+
+ 1955
+
+
+ 1954
+
+
+ 1953
+
+
+ 1952
+
+
+ 1951
+
+
+ 1950
+
+
+ 1949
+
+
+ 1948
+
+
+ 1947
+
+
+ 1946
+
+
+ 1945
+
+
+ 1944
+
+
+ 1943
+
+
+ 1942
+
+
+ 1941
+
+
+ 1940
+
+
+ 1939
+
+
+ 1938
+
+
+ 1937
+
+
+ 1936
+
+
+ 1935
+
+
+ 1934
+
+
+ 1933
+
+
+ 1932
+
+
+ 1931
+
+
+ 1930
+
+
+ 1929
+
+
+ 1928
+
+
+ 1927
+
+
+ 1926
+
+
+ 1925
+
+
+ 1924
+
+
+ 1923
+
+
+ 1922
+
+
+ 1921
+
+
+ 1920
+
+
+ 1919
+
+
+ 1918
+
+
+ 1917
+
+
+ 1916
+
+
+ 1915
+
+
+ 1914
+
+
+ 1913
+
+
+ 1912
+
+
+ 1911
+
+
+ 1910
+
+
+ 1909
+
+
+ 1908
+
+
+ 1907
+
+
+ 1906
+
+
+ 1905
+
+
+ 1904
+
+
+ 1903
+
+
+ 1902
+
+
+ 1901
+
+
+ 1900
+
+
+
+`;
diff --git a/src/components/VDatePicker/index.js b/src/components/VDatePicker/index.js
index a2dc5db22194..6ee50d37dcde 100644
--- a/src/components/VDatePicker/index.js
+++ b/src/components/VDatePicker/index.js
@@ -1,8 +1,27 @@
import VDatePicker from './VDatePicker'
+import VDatePickerTitle from './VDatePickerTitle'
+import VDatePickerHeader from './VDatePickerHeader'
+import VDatePickerDateTable from './VDatePickerDateTable'
+import VDatePickerMonthTable from './VDatePickerMonthTable'
+import VDatePickerYears from './VDatePickerYears'
+
+export {
+ VDatePicker,
+ VDatePickerTitle,
+ VDatePickerHeader,
+ VDatePickerDateTable,
+ VDatePickerMonthTable,
+ VDatePickerYears
+}
/* istanbul ignore next */
VDatePicker.install = function install (Vue) {
Vue.component(VDatePicker.name, VDatePicker)
+ Vue.component(VDatePickerTitle.name, VDatePickerTitle)
+ Vue.component(VDatePickerHeader.name, VDatePickerHeader)
+ Vue.component(VDatePickerDateTable.name, VDatePickerDateTable)
+ Vue.component(VDatePickerMonthTable.name, VDatePickerMonthTable)
+ Vue.component(VDatePickerYears.name, VDatePickerYears)
}
export default VDatePicker
diff --git a/src/components/VDatePicker/mixins/date-header.js b/src/components/VDatePicker/mixins/date-header.js
deleted file mode 100644
index 4615132bdebe..000000000000
--- a/src/components/VDatePicker/mixins/date-header.js
+++ /dev/null
@@ -1,64 +0,0 @@
-export default {
- methods: {
- genBtn (change, children) {
- return this.$createElement('v-btn', {
- props: {
- dark: this.dark,
- icon: true
- },
- nativeOn: {
- click: e => {
- e.stopPropagation()
- if (this.activePicker === 'DATE') {
- this.updateTableMonth(change)
- } else if (this.activePicker === 'MONTH') {
- this.tableDate = `${change}`
- }
- }
- }
- }, children)
- },
-
- genHeader (keyValue, selectorText) {
- const header = this.$createElement('strong', {
- 'class': this.addTextColorClassChecks(),
- key: keyValue,
- on: {
- click: () => this.activePicker = this.activePicker === 'DATE' ? 'MONTH' : 'YEAR'
- }
- }, selectorText)
-
- const transition = this.$createElement('transition', {
- props: { name: this.computedTransition }
- }, [header])
-
- return this.$createElement('div', {
- 'class': 'picker--date__header-selector-date'
- }, [transition])
- },
-
- genSelector () {
- const keyValue = this.activePicker === 'DATE' ? this.tableMonth : this.tableYear
- // Generates the text of the button switching the active picker in the table header.
- // For date picker it uses headerDateFormat formatting function (defined by dev or
- // default). For month picker it uses Date::toLocaleDateString to get the year
- // in the current locale or just a year numeric value if Date::toLocaleDateString
- // is not supported
- const selectorText = this.activePicker === 'DATE'
- ? this.formatters.headerDate(`${this.tableYear}-${this.tableMonth + 1}`)
- : this.formatters.year(`${this.tableYear}`)
-
- return this.$createElement('div', {
- 'class': 'picker--date__header-selector'
- }, [
- this.genBtn(keyValue - 1, [
- this.$createElement('v-icon', 'chevron_left')
- ]),
- this.genHeader(keyValue, selectorText),
- this.genBtn(keyValue + 1, [
- this.$createElement('v-icon', 'chevron_right')
- ])
- ])
- }
- }
-}
diff --git a/src/components/VDatePicker/mixins/date-picker-table.js b/src/components/VDatePicker/mixins/date-picker-table.js
new file mode 100644
index 000000000000..a1cf4f5fe563
--- /dev/null
+++ b/src/components/VDatePicker/mixins/date-picker-table.js
@@ -0,0 +1,126 @@
+import '../../../stylus/components/_date-picker-table.styl'
+
+// Directives
+import Touch from '../../../directives/touch'
+
+// Utils
+import isDateAllowed from '.././util/isDateAllowed'
+
+export default {
+ directives: { Touch },
+
+ data () {
+ return {
+ defaultColor: 'accent',
+ isReversing: false
+ }
+ },
+
+ props: {
+ allowedDates: Function,
+ current: String,
+ disabled: Boolean,
+ format: {
+ type: Function,
+ default: null
+ },
+ locale: {
+ type: String,
+ default: 'en-us'
+ },
+ min: String,
+ max: String,
+ scrollable: Boolean,
+ tableDate: {
+ type: String,
+ required: true
+ },
+ value: {
+ type: String,
+ required: false
+ }
+ },
+
+ computed: {
+ computedTransition () {
+ return this.isReversing ? 'tab-reverse-transition' : 'tab-transition'
+ },
+ displayedMonth () {
+ return this.tableDate.split('-')[1] - 1
+ },
+ displayedYear () {
+ return this.tableDate.split('-')[0] * 1
+ }
+ },
+
+ watch: {
+ tableDate (newVal, oldVal) {
+ this.isReversing = newVal < oldVal
+ }
+ },
+
+ methods: {
+ genButtonClasses (value, isDisabled, isFloating) {
+ const isSelected = value === this.value
+ const isCurrent = value === this.current
+
+ const classes = {
+ 'btn--active': isSelected,
+ 'btn--flat': !isSelected,
+ 'btn--icon': isSelected && !isDisabled && isFloating,
+ 'btn--floating': isFloating,
+ 'btn--depressed': !isFloating && isSelected,
+ 'btn--disabled': isDisabled || (this.disabled && isSelected),
+ 'btn--outline': isCurrent && !isSelected
+ }
+
+ if (isSelected) return this.addBackgroundColorClassChecks(classes)
+ if (isCurrent) return this.addTextColorClassChecks(classes)
+ return classes
+ },
+ genButton (value, isFloating) {
+ const isDisabled = !isDateAllowed(value, this.min, this.max, this.allowedDates)
+
+ return this.$createElement('button', {
+ staticClass: 'btn',
+ 'class': this.genButtonClasses(value, isDisabled, isFloating),
+ attrs: {
+ type: 'button'
+ },
+ domProps: {
+ disabled: isDisabled,
+ innerHTML: `${this.formatter(value)}
`
+ },
+ on: isDisabled ? {} : {
+ click: () => this.$emit('input', value)
+ }
+ })
+ },
+ wheel (e) {
+ e.preventDefault()
+ this.$emit('tableDate', this.calculateTableDate(e.deltaY))
+ },
+ touch (value) {
+ this.$emit('tableDate', this.calculateTableDate(value))
+ },
+ genTable (staticClass, children) {
+ const transition = this.$createElement('transition', {
+ props: { name: this.computedTransition }
+ }, [this.$createElement('table', { key: this.tableDate }, children)])
+
+ const touchDirective = {
+ name: 'touch',
+ value: {
+ left: e => (e.offsetX < -15) && this.touch(1),
+ right: e => (e.offsetX > 15) && this.touch(-1)
+ }
+ }
+
+ return this.$createElement('div', {
+ staticClass,
+ on: this.scrollable ? { wheel: this.wheel } : undefined,
+ directives: [touchDirective]
+ }, [transition])
+ }
+ }
+}
diff --git a/src/components/VDatePicker/mixins/date-table.js b/src/components/VDatePicker/mixins/date-table.js
deleted file mode 100644
index c5e2cb3c5072..000000000000
--- a/src/components/VDatePicker/mixins/date-table.js
+++ /dev/null
@@ -1,95 +0,0 @@
-export default {
- methods: {
- dateWheelScroll (e) {
- e.preventDefault()
-
- this.updateTableMonth(e.deltaY < 0 ? this.tableMonth + 1 : this.tableMonth - 1)
- },
- dateGenTHead () {
- const days = this.weekDays.map(day => this.$createElement('th', day))
- return this.$createElement('thead', this.dateGenTR(days))
- },
- dateClick (day) {
- this.inputDate = this.sanitizeDateString(`${this.tableYear}-${this.tableMonth + 1}-${day}`, 'date')
- this.$nextTick(() => (this.autosave && this.save()))
- },
- dateGenTD (day) {
- const date = this.sanitizeDateString(`${this.tableYear}-${this.tableMonth + 1}-${day}`, 'date')
- const buttonText = this.formatters.day(date)
- const isActive = this.dateIsActive(day)
- const isCurrent = this.dateIsCurrent(day)
- const classes = Object.assign({
- 'btn--active': isActive,
- 'btn--outline': isCurrent && !isActive,
- 'btn--disabled': !this.isAllowed(date)
- }, this.themeClasses)
-
- const button = this.$createElement('button', {
- staticClass: 'btn btn--raised btn--icon',
- 'class': (isActive || isCurrent)
- ? this.addBackgroundColorClassChecks(classes)
- : classes,
- attrs: {
- type: 'button'
- },
- domProps: {
- innerHTML: `${buttonText} `
- },
- on: {
- click: () => this.dateClick(day)
- }
- })
-
- return this.$createElement('td', [button])
- },
- // Returns number of the days from the firstDayOfWeek to the first day of the current month
- weekDaysBeforeFirstDayOfTheMonth () {
- const pad = n => (n * 1 < 10) ? `0${n * 1}` : `${n}`
- const firstDayOfTheMonth = new Date(`${this.tableYear}-${pad(this.tableMonth + 1)}-01T00:00:00+00:00`)
- const weekDay = firstDayOfTheMonth.getUTCDay()
- return (weekDay - parseInt(this.firstDayOfWeek) + 7) % 7
- },
- dateGenTBody () {
- const children = []
- const daysInMonth = new Date(this.tableYear, this.tableMonth + 1, 0).getDate()
- let rows = []
- const day = this.weekDaysBeforeFirstDayOfTheMonth()
-
- for (let i = 0; i < day; i++) {
- rows.push(this.$createElement('td'))
- }
-
- for (let i = 1; i <= daysInMonth; i++) {
- rows.push(this.dateGenTD(i))
-
- if (rows.length % 7 === 0) {
- children.push(this.dateGenTR(rows))
- rows = []
- }
- }
-
- if (rows.length) {
- children.push(this.dateGenTR(rows))
- }
-
- children.length < 6 && children.push(this.dateGenTR([
- this.$createElement('td', { domProps: { innerHTML: ' ' } })
- ]))
-
- return this.$createElement('tbody', children)
- },
- dateGenTR (children = [], data = {}) {
- return [this.$createElement('tr', data, children)]
- },
- dateIsActive (i) {
- return this.tableYear === this.year &&
- this.tableMonth === this.month &&
- this.day === i
- },
- dateIsCurrent (i) {
- return this.currentYear === this.tableYear &&
- this.currentMonth === this.tableMonth &&
- this.currentDay === i
- }
- }
-}
diff --git a/src/components/VDatePicker/mixins/date-title.js b/src/components/VDatePicker/mixins/date-title.js
deleted file mode 100644
index 909c3560ad79..000000000000
--- a/src/components/VDatePicker/mixins/date-title.js
+++ /dev/null
@@ -1,68 +0,0 @@
-
-export default {
- methods: {
- genYearIcon () {
- return this.yearIcon
- ? this.$createElement('v-icon', {
- props: {
- dark: true
- }
- }, this.yearIcon)
- : null
- },
-
- getYearBtn () {
- return this.$createElement('div', {
- 'class': {
- 'picker--date__title-year': true,
- 'active': this.activePicker === 'YEAR'
- },
- on: {
- click: e => {
- e.stopPropagation()
- this.activePicker = 'YEAR'
- }
- }
- }, [
- this.formatters.year(`${this.year}`),
- this.genYearIcon()
- ])
- },
-
- genTitleText (title) {
- return this.$createElement('transition', {
- props: {
- name: 'slide-y-reverse-transition',
- mode: 'out-in'
- }
- }, [
- this.$createElement('div', {
- domProps: { innerHTML: title },
- key: title
- })
- ])
- },
-
- genTitleDate (title) {
- return this.$createElement('div', {
- staticClass: 'picker--date__title-date',
- 'class': {
- 'active': this.activePicker === this.type.toUpperCase()
- },
- on: {
- click: e => {
- e.stopPropagation()
- this.activePicker = this.type.toUpperCase()
- }
- }
- }, [this.genTitleText(title)])
- },
-
- genTitle (title) {
- return this.genPickerTitle([
- this.getYearBtn(),
- this.genTitleDate(title)
- ])
- }
- }
-}
diff --git a/src/components/VDatePicker/mixins/date-years.js b/src/components/VDatePicker/mixins/date-years.js
deleted file mode 100644
index dbabf7582635..000000000000
--- a/src/components/VDatePicker/mixins/date-years.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export default {
- methods: {
- genYears () {
- return this.$createElement('ul', {
- staticClass: 'picker--date__years',
- key: 'year',
- ref: 'years'
- }, this.genYearItems())
- },
- yearClick (year) {
- if (this.type === 'year') {
- this.inputDate = `${year}`
- this.$nextTick(() => (this.autosave && this.save()))
- } else if (this.type === 'month') {
- const date = this.sanitizeDateString(`${year}-${this.month + 1}`, 'month')
- if (this.isAllowed(date)) this.inputDate = date
- this.tableDate = `${year}`
- this.activePicker = 'MONTH'
- } else {
- const date = this.sanitizeDateString(`${year}-${this.tableMonth + 1}-${this.day}`, 'date')
- if (this.isAllowed(date)) this.inputDate = date
- this.tableDate = `${year}-${this.tableMonth + 1}`
- this.activePicker = 'MONTH'
- }
- },
- genYearItems () {
- const children = []
- for (let year = this.year + 100, length = this.year - 100; year > length; year--) {
- const buttonText = this.formatters.year(`${year}`)
-
- children.push(this.$createElement('li', {
- 'class': this.year === year
- ? this.addTextColorClassChecks({ active: true })
- : {},
- on: {
- click: () => this.yearClick(year)
- }
- }, buttonText))
- }
- return children
- }
- }
-}
diff --git a/src/components/VDatePicker/mixins/month-table.js b/src/components/VDatePicker/mixins/month-table.js
deleted file mode 100644
index d18ff1680398..000000000000
--- a/src/components/VDatePicker/mixins/month-table.js
+++ /dev/null
@@ -1,78 +0,0 @@
-export default {
- methods: {
- monthWheelScroll (e) {
- e.preventDefault()
-
- let year = this.tableYear
-
- if (e.deltaY < 0) year++
- else year--
-
- this.tableDate = `${year}`
- },
- monthClick (month) {
- // Updates inputDate setting 'YYYY-MM' or 'YYYY-MM-DD' format, depending on the picker type
- if (this.type === 'date') {
- const date = this.sanitizeDateString(`${this.tableYear}-${month + 1}-${this.day}`, 'date')
- if (this.isAllowed(date)) this.inputDate = date
- this.updateTableMonth(month)
- this.activePicker = 'DATE'
- } else {
- this.inputDate = this.sanitizeDateString(`${this.tableYear}-${month + 1}`, 'month')
- this.$nextTick(() => (this.autosave && this.save()))
- }
- },
- monthGenTD (month) {
- const pad = n => (n * 1 < 10) ? `0${n * 1}` : `${n}`
- const date = `${this.tableYear}-${pad(month + 1)}`
- const monthName = this.formatters.month(date)
- const isActive = this.monthIsActive(month)
- const isCurrent = this.monthIsCurrent(month)
- const classes = Object.assign({
- 'btn--flat': !isActive,
- 'btn--active': isActive,
- 'btn--outline': isCurrent && !isActive,
- 'btn--disabled': this.type === 'month' && !this.isAllowed(date)
- }, this.themeClasses)
-
- return this.$createElement('td', [
- this.$createElement('button', {
- staticClass: 'btn',
- 'class': (isActive || isCurrent)
- ? this.addBackgroundColorClassChecks(classes)
- : classes,
- attrs: {
- type: 'button'
- },
- domProps: {
- innerHTML: `${monthName} `
- },
- on: {
- click: () => this.monthClick(month)
- }
- })
- ])
- },
- monthGenTBody () {
- const children = []
- const cols = Array(3).fill(null)
- const rows = 12 / cols.length
-
- for (let row = 0; row < rows; row++) {
- children.push(this.$createElement('tr', cols.map((_, col) => {
- return this.monthGenTD(row * cols.length + col)
- })))
- }
-
- return this.$createElement('tbody', children)
- },
- monthIsActive (i) {
- return this.tableYear === this.year &&
- (this.type === 'month' ? this.month : this.tableMonth) === i
- },
- monthIsCurrent (i) {
- return this.currentYear === this.tableYear &&
- this.currentMonth === i
- }
- }
-}
diff --git a/src/components/VDatePicker/util/createNativeLocaleFormatter.js b/src/components/VDatePicker/util/createNativeLocaleFormatter.js
new file mode 100644
index 000000000000..680c3fc73d36
--- /dev/null
+++ b/src/components/VDatePicker/util/createNativeLocaleFormatter.js
@@ -0,0 +1,15 @@
+import pad from './pad'
+
+export default (locale, options, { start, length } = { start: 0, length: 0 }) => {
+ const makeIsoString = dateString => {
+ const [year, month, date] = dateString.trim().split(' ')[0].split('-')
+ return [year, pad(month || 1), pad(date || 1)].join('-')
+ }
+
+ try {
+ const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options)
+ return dateString => intlFormatter.format(new Date(`${makeIsoString(dateString)}T00:00:00+00:00`))
+ } catch (e) {
+ return (start || length) ? dateString => makeIsoString(dateString).substr(start, length) : null
+ }
+}
diff --git a/src/components/VDatePicker/util/createNativeLocaleFormatter.spec.js b/src/components/VDatePicker/util/createNativeLocaleFormatter.spec.js
new file mode 100644
index 000000000000..98e30971fa76
--- /dev/null
+++ b/src/components/VDatePicker/util/createNativeLocaleFormatter.spec.js
@@ -0,0 +1,24 @@
+import createNativeLocaleFormatter from './createNativeLocaleFormatter'
+import { test } from '@util/testing'
+
+test('VDatePicker/util/createNativeLocaleFormatter.js', ({ mount }) => {
+ it('should format dates', () => {
+
+ const formatter = createNativeLocaleFormatter(undefined, { day: 'numeric', timeZone: 'UTC' })
+ expect(formatter('2013-2-07')).toBe('7')
+ })
+
+ it('should format dates if Intl is not defined', () => {
+ const oldIntl = global.Intl
+
+ global.Intl = null
+ const formatter = createNativeLocaleFormatter(undefined, { day: 'numeric', timeZone: 'UTC' }, { start: 0, length: 10 })
+ expect(formatter('2013-2-7')).toBe('2013-02-07')
+ expect(formatter('2013-2')).toBe('2013-02-01')
+ expect(formatter('2013')).toBe('2013-01-01')
+
+ const nullFormatter = createNativeLocaleFormatter(undefined, { day: 'numeric', timeZone: 'UTC' })
+ expect(nullFormatter).toBe(null)
+ global.Intl = oldIntl
+ })
+ })
diff --git a/src/components/VDatePicker/util/index.js b/src/components/VDatePicker/util/index.js
new file mode 100644
index 000000000000..a69aca1100fe
--- /dev/null
+++ b/src/components/VDatePicker/util/index.js
@@ -0,0 +1,9 @@
+import createNativeLocaleFormatter from './createNativeLocaleFormatter'
+import monthChange from './monthChange'
+import pad from './pad'
+
+export {
+ createNativeLocaleFormatter,
+ monthChange,
+ pad
+}
diff --git a/src/components/VDatePicker/util/isDateAllowed.js b/src/components/VDatePicker/util/isDateAllowed.js
new file mode 100644
index 000000000000..b38a2279d175
--- /dev/null
+++ b/src/components/VDatePicker/util/isDateAllowed.js
@@ -0,0 +1,5 @@
+export default function isDateAllowed (date, min, max, allowedFn) {
+ return (!allowedFn || allowedFn(date)) &&
+ (!min || date >= min) &&
+ (!max || date <= max)
+}
diff --git a/src/components/VDatePicker/util/monthChange.js b/src/components/VDatePicker/util/monthChange.js
new file mode 100644
index 000000000000..3351115824d7
--- /dev/null
+++ b/src/components/VDatePicker/util/monthChange.js
@@ -0,0 +1,17 @@
+import pad from './pad'
+
+/**
+ * @param {String} value YYYY-MM format
+ * @param {Number} sign -1 or +1
+ */
+export default (value, sign) => {
+ const [year, month] = value.split('-').map(v => 1 * v)
+
+ if (month + sign === 0) {
+ return `${year - 1}-12`
+ } else if (month + sign === 13) {
+ return `${year + 1}-01`
+ } else {
+ return `${year}-${pad(month + sign)}`
+ }
+}
diff --git a/src/components/VDatePicker/util/monthChange.spec.js b/src/components/VDatePicker/util/monthChange.spec.js
new file mode 100644
index 000000000000..30712e88ce72
--- /dev/null
+++ b/src/components/VDatePicker/util/monthChange.spec.js
@@ -0,0 +1,11 @@
+import monthChange from './monthChange'
+import { test } from '@util/testing'
+
+test('VDatePicker/util/monthChange.js', ({ mount }) => {
+ it('should change month', () => {
+ expect(monthChange('2000-01', -1)).toBe('1999-12')
+ expect(monthChange('2000-01', +1)).toBe('2000-02')
+ expect(monthChange('2000-12', -1)).toBe('2000-11')
+ expect(monthChange('2000-12', +1)).toBe('2001-01')
+ })
+})
diff --git a/src/components/VDatePicker/util/pad.js b/src/components/VDatePicker/util/pad.js
new file mode 100644
index 000000000000..c7d440ef6a78
--- /dev/null
+++ b/src/components/VDatePicker/util/pad.js
@@ -0,0 +1,16 @@
+const padStart = (string, targetLength, padString) => {
+ targetLength = targetLength >> 0
+ string = String(string)
+ padString = String(padString)
+ if (string.length > targetLength) {
+ return String(string)
+ }
+
+ targetLength = targetLength - string.length
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length)
+ }
+ return padString.slice(0, targetLength) + String(string)
+}
+
+export default (n, length = 2) => padStart(n, length, '0')
diff --git a/src/components/VDatePicker/util/pad.spec.js b/src/components/VDatePicker/util/pad.spec.js
new file mode 100644
index 000000000000..5d9b4d163d1e
--- /dev/null
+++ b/src/components/VDatePicker/util/pad.spec.js
@@ -0,0 +1,19 @@
+import pad from './pad'
+import { test } from '@util/testing'
+
+test('VDatePicker/util/pad.js', ({ mount }) => {
+ it('should pad 1-digit numbers', () => {
+ expect(pad(0)).toBe('00')
+ expect(pad('3', 3)).toBe('003')
+ })
+
+ it('should pad 2-digit numbers', () => {
+ expect(pad(40)).toBe('40')
+ expect(pad('98')).toBe('98')
+ })
+
+ it('should not pad 3-digit numbers', () => {
+ expect(pad(400)).toBe('400')
+ expect(pad('998')).toBe('998')
+ })
+})
diff --git a/src/components/VDialog/VDialog.js b/src/components/VDialog/VDialog.js
index 5d496857caee..51922852fb04 100644
--- a/src/components/VDialog/VDialog.js
+++ b/src/components/VDialog/VDialog.js
@@ -1,9 +1,10 @@
-require('../../stylus/components/_dialogs.styl')
+import '../../stylus/components/_dialogs.styl'
// Mixins
import Dependent from '../../mixins/dependent'
import Detachable from '../../mixins/detachable'
import Overlayable from '../../mixins/overlayable'
+import Returnable from '../../mixins/returnable'
import Stackable from '../../mixins/stackable'
import Toggleable from '../../mixins/toggleable'
@@ -16,7 +17,14 @@ import { getZIndex } from '../../util/helpers'
export default {
name: 'v-dialog',
- mixins: [Dependent, Detachable, Overlayable, Stackable, Toggleable],
+ mixins: [
+ Dependent,
+ Detachable,
+ Overlayable,
+ Returnable,
+ Stackable,
+ Toggleable
+ ],
directives: {
ClickOutside
@@ -61,7 +69,6 @@ export default {
'dialog--active': this.isActive,
'dialog--persistent': this.persistent,
'dialog--fullscreen': this.fullscreen,
- 'dialog--stacked-actions': this.stackedActions && !this.fullscreen,
'dialog--scrollable': this.scrollable
}
},
@@ -97,7 +104,7 @@ export default {
closeConditional (e) {
// close dialog if !persistent, clicked outside and we're the topmost dialog.
// Since this should only be called in a capture event (bottom up), we shouldn't need to stop propagation
- return !this.persistent &&
+ return this.isActive && !this.persistent &&
getZIndex(this.$refs.content) >= this.getMaxZIndex() &&
!this.$refs.content.contains(e.target)
},
@@ -126,14 +133,17 @@ export default {
directives: [
{
name: 'click-outside',
- value: {
- callback: this.closeConditional,
+ value: () => (this.isActive = false),
+ args: {
+ closeConditional: this.closeConditional,
include: this.getOpenDependentElements
}
},
{ name: 'show', value: this.isActive }
],
- on: { click: e => e.stopPropagation() }
+ on: {
+ click: e => { e.stopPropagation() }
+ }
}
if (!this.fullscreen) {
@@ -148,6 +158,7 @@ export default {
'class': 'dialog__activator',
on: {
click: e => {
+ e.stopPropagation()
if (!this.disabled) this.isActive = !this.isActive
}
}
@@ -171,9 +182,9 @@ export default {
}, [dialog]))
return h('div', {
- 'class': 'dialog__container',
+ staticClass: 'dialog__container',
style: {
- display: !this.$slots.activator && 'none' || this.fullWidth ? 'block' : 'inline-block'
+ display: (!this.$slots.activator || this.fullWidth) ? 'block' : 'inline-block'
}
}, children)
}
diff --git a/src/components/VDialog/VDialog.spec.js b/src/components/VDialog/VDialog.spec.js
index 233f2b08aa9c..d11244228b5c 100644
--- a/src/components/VDialog/VDialog.spec.js
+++ b/src/components/VDialog/VDialog.spec.js
@@ -1,13 +1,12 @@
-import VDialog from '~components/VDialog'
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
+import VDialog from '@components/VDialog'
+import { test } from '@util/testing'
-test('VDialog.js', () => {
+test('VDialog.js', ({ mount, compileToFunctions }) => {
it('should render component and match snapshot', () => {
const wrapper = mount(VDialog)
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a disabled component and match snapshot', () => {
@@ -18,7 +17,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a persistent component and match snapshot', () => {
@@ -29,7 +28,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a fullscreen component and match snapshot', () => {
@@ -40,7 +39,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a lazy component and match snapshot', () => {
@@ -51,7 +50,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render a scrollable component and match snapshot', () => {
@@ -62,7 +61,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render component with custom origin and match snapshot', () => {
@@ -73,7 +72,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render component with custom width (max-width) and match snapshot', () => {
@@ -84,7 +83,7 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
it('should render component with custom transition and match snapshot', () => {
@@ -95,6 +94,94 @@ test('VDialog.js', () => {
})
expect(wrapper.html()).toMatchSnapshot()
- expect('Application is missing component.').toHaveBeenTipped()
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should open dialog on activator click', async () => {
+ const input = jest.fn()
+ const wrapper = mount(VDialog, {
+ slots: {
+ activator: [compileToFunctions('activator ')]
+ }
+ })
+
+ wrapper.vm.$on('input', input)
+
+ expect(wrapper.vm.isActive).toBe(false)
+ wrapper.find('.dialog__activator')[0].trigger('click')
+ expect(wrapper.vm.isActive).toBe(true)
+ await wrapper.vm.$nextTick()
+ expect(input).toBeCalledWith(true)
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('not should open disabed dialog on activator click', async () => {
+ const input = jest.fn()
+ const wrapper = mount(VDialog, {
+ propsData: {
+ disabled: true
+ },
+ slots: {
+ activator: [compileToFunctions('activator ')]
+ }
+ })
+
+ wrapper.vm.$on('input', input)
+
+ expect(wrapper.vm.isActive).toBe(false)
+ wrapper.find('.dialog__activator')[0].trigger('click')
+ expect(wrapper.vm.isActive).toBe(false)
+ await wrapper.vm.$nextTick()
+ expect(input).not.toBeCalled()
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('not change state on v-model update', async () => {
+ const wrapper = mount(VDialog, {
+ propsData: {
+ value: false
+ },
+ slots: {
+ activator: [compileToFunctions('activator ')]
+ }
+ })
+
+ expect(wrapper.vm.isActive).toBe(false)
+
+ wrapper.setProps({
+ value: true
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.isActive).toBe(true)
+
+ wrapper.setProps({
+ value: false
+ })
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.isActive).toBe(false)
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
+ })
+
+ it('should emit keydown event', async () => {
+ const keydown = jest.fn()
+ const component = {
+ render: h => h(VDialog, {
+ props: {
+ value: true
+ },
+ on: {
+ keydown
+ }
+ })
+ }
+ const wrapper = mount(component)
+
+ window.dispatchEvent(new Event('keydown'))
+ expect(keydown).toBeCalled()
+
+ expect('Unable to locate target [data-app]').toHaveBeenTipped()
})
})
diff --git a/src/components/VDivider/VDivider.js b/src/components/VDivider/VDivider.js
index 07759dacf9db..d5099619c03a 100644
--- a/src/components/VDivider/VDivider.js
+++ b/src/components/VDivider/VDivider.js
@@ -1,4 +1,4 @@
-require('../../stylus/components/_dividers.styl')
+import '../../stylus/components/_dividers.styl'
import Themeable from '../../mixins/themeable'
diff --git a/src/components/VDivider/VDivider.spec.js b/src/components/VDivider/VDivider.spec.js
index 6231b35e6967..dbf104b5601c 100644
--- a/src/components/VDivider/VDivider.spec.js
+++ b/src/components/VDivider/VDivider.spec.js
@@ -1,21 +1,40 @@
-import { mount } from 'avoriaz'
-import VDivider from '~components/VDivider'
-import { test, functionalContext } from '~util/testing'
+import VDivider from '@components/VDivider'
+import { test } from '@util/testing'
-test('VDivider.js', () => {
+test('VDivider.js', ({ mount, compileToFunctions, functionalContext }) => {
it('should render component and match snapshot', () => {
const wrapper = mount(VDivider, functionalContext())
expect(wrapper.html()).toMatchSnapshot()
})
- it('should render an inset component and match snapshot', () => {
+ it('should render an inset component', () => {
const wrapper = mount(VDivider, functionalContext({
- propsData: {
+ props: {
inset: true
}
}))
- expect(wrapper.html()).toMatchSnapshot()
+ expect(wrapper.hasClass('divider--inset')).toBe(true)
+ })
+
+ it('should render a light component', () => {
+ const wrapper = mount(VDivider, functionalContext({
+ props: {
+ light: true
+ }
+ }))
+
+ expect(wrapper.hasClass('theme--light')).toBe(true)
+ })
+
+ it('should render a dark component', () => {
+ const wrapper = mount(VDivider, functionalContext({
+ props: {
+ dark: true
+ }
+ }))
+
+ expect(wrapper.hasClass('theme--dark')).toBe(true)
})
})
diff --git a/src/components/VDivider/__snapshots__/VDivider.spec.js.snap b/src/components/VDivider/__snapshots__/VDivider.spec.js.snap
index d8fc695ef55f..0a39b1ac927b 100644
--- a/src/components/VDivider/__snapshots__/VDivider.spec.js.snap
+++ b/src/components/VDivider/__snapshots__/VDivider.spec.js.snap
@@ -1,11 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`VDivider.js should render an inset component and match snapshot 1`] = `
-
-
-
-`;
-
exports[`VDivider.js should render component and match snapshot 1`] = `
diff --git a/src/components/VExpansionPanel/VExpansionPanel.js b/src/components/VExpansionPanel/VExpansionPanel.js
index 901f74334915..ed606ee7dc1b 100644
--- a/src/components/VExpansionPanel/VExpansionPanel.js
+++ b/src/components/VExpansionPanel/VExpansionPanel.js
@@ -1,11 +1,12 @@
-require('../../stylus/components/_expansion-panel.styl')
+import '../../stylus/components/_expansion-panel.styl'
import Themeable from '../../mixins/themeable'
+import { provide as RegistrableProvide } from '../../mixins/registrable'
export default {
name: 'v-expansion-panel',
- mixins: [Themeable],
+ mixins: [Themeable, RegistrableProvide('expansionPanel')],
provide () {
return {
@@ -14,6 +15,12 @@ export default {
}
},
+ data () {
+ return {
+ items: []
+ }
+ },
+
props: {
expand: Boolean,
focusable: Boolean,
@@ -22,22 +29,26 @@ export default {
},
methods: {
- getChildren () {
- return this.$children.filter(c => {
- if (!c.$options) return
-
- return c.$options.name === 'v-expansion-panel-content'
- })
- },
panelClick (uid) {
if (!this.expand) {
- return this.getChildren()
- .forEach(e => e.toggle(uid))
+ for (let i = 0; i < this.items.length; i++) {
+ this.items[i].toggle(uid)
+ }
+ return
}
- const panel = this.$children.find(e => e._uid === uid)
-
- panel && panel.toggle(uid)
+ for (let i = 0; i < this.items.length; i++) {
+ if (this.items[i].uid === uid) {
+ this.items[i].toggle(uid)
+ return
+ }
+ }
+ },
+ register (uid, toggle) {
+ this.items.push({ uid, toggle })
+ },
+ unregister (uid) {
+ this.items = this.items.filter(i => i.uid !== uid)
}
},
diff --git a/src/components/VExpansionPanel/VExpansionPanel.spec.js b/src/components/VExpansionPanel/VExpansionPanel.spec.js
index 8ea1b5aeea2a..8c39a3cdba46 100644
--- a/src/components/VExpansionPanel/VExpansionPanel.spec.js
+++ b/src/components/VExpansionPanel/VExpansionPanel.spec.js
@@ -1,22 +1,57 @@
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
-import VExpansionPanel from '~components/VExpansionPanel'
+import Vue from 'vue'
+import { test } from '@util/testing'
+import VExpansionPanel from '@components/VExpansionPanel'
+import { VExpansionPanelContent } from '@components/VExpansionPanel'
-// TODO: Fix when Vue has optional injects
-test.skip('VExpansionPanel.js', () => {
- it('should render component and match snapshot', () => {
- const wrapper = mount(VExpansionPanel)
+const createPanel = props => {
+ return Vue.component('test', {
+ components: { VExpansionPanel, VExpansionPanelContent },
+ render: h => {
+ const panelContent = h('v-expansion-panel-content', [
+ h('div', { slot: 'header' }, 'header'),
+ 'content'
+ ])
+ return h('v-expansion-panel', { props }, [panelContent])
+ }
+ })
+}
+
+test('VExpansionPanel.js', ({ mount, compileToFunctions }) => {
+ it('should render component and match snapshot', async () => {
+ const wrapper = mount(createPanel())
+
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.find('.expansion-panel__header')[0].trigger('click')
+ await wrapper.vm.$nextTick()
expect(wrapper.html()).toMatchSnapshot()
})
- it('should render an expanded component and match snapshot', () => {
- const wrapper = mount(VExpansionPanel, {
- propsData: {
- expand: true
- }
- })
+ it('should render inset component', () => {
+ const wrapper = mount(createPanel({
+ inset: true
+ }))
+
+ expect(wrapper.hasClass('expansion-panel--inset')).toBe(true)
+ })
+
+ it('should render popout component', () => {
+ const wrapper = mount(createPanel({
+ popout: true
+ }))
+
+ expect(wrapper.hasClass('expansion-panel--popout')).toBe(true)
+ })
+
+ it('should render an expanded component and match snapshot', async () => {
+ const wrapper = mount(createPanel({
+ expand: true
+ }))
+
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.find('.expansion-panel__header')[0].trigger('click')
+ await wrapper.vm.$nextTick()
expect(wrapper.html()).toMatchSnapshot()
})
})
diff --git a/src/components/VExpansionPanel/VExpansionPanelContent.js b/src/components/VExpansionPanel/VExpansionPanelContent.js
index f6cec4de0e61..35b7a1ca5b02 100644
--- a/src/components/VExpansionPanel/VExpansionPanelContent.js
+++ b/src/components/VExpansionPanel/VExpansionPanelContent.js
@@ -2,23 +2,23 @@ import { VExpandTransition } from '../transitions'
import Bootable from '../../mixins/bootable'
import Toggleable from '../../mixins/toggleable'
+import Rippleable from '../../mixins/rippleable'
+import { inject as RegistrableInject } from '../../mixins/registrable'
import VIcon from '../VIcon'
-import Ripple from '../../directives/ripple'
import ClickOutside from '../../directives/click-outside'
export default {
name: 'v-expansion-panel-content',
- mixins: [Bootable, Toggleable],
+ mixins: [Bootable, Toggleable, Rippleable, RegistrableInject('expansionPanel', 'v-expansion-panel', 'v-expansion-panel-content')],
components: {
VIcon
},
directives: {
- Ripple,
ClickOutside
},
@@ -31,8 +31,15 @@ export default {
},
props: {
+ expandIcon: {
+ type: String,
+ default: 'keyboard_arrow_down'
+ },
hideActions: Boolean,
- ripple: Boolean
+ ripple: {
+ type: [Boolean, Object],
+ default: false
+ }
},
methods: {
@@ -67,7 +74,7 @@ export default {
if (this.hideActions) return null
const icon = this.$slots.actions ||
- this.$createElement('v-icon', 'keyboard_arrow_down')
+ this.$createElement('v-icon', this.expandIcon)
return this.$createElement('div', {
staticClass: 'header__icon'
@@ -84,6 +91,14 @@ export default {
}
},
+ mounted () {
+ this.expansionPanel.register(this._uid, this.toggle)
+ },
+
+ beforeDestroy () {
+ this.expansionPanel.unregister(this._uid)
+ },
+
render (h) {
const children = []
diff --git a/src/components/VExpansionPanel/VExpansionPanelContent.spec.js b/src/components/VExpansionPanel/VExpansionPanelContent.spec.js
index c44c8df7d13f..2f0c2847a856 100644
--- a/src/components/VExpansionPanel/VExpansionPanelContent.spec.js
+++ b/src/components/VExpansionPanel/VExpansionPanelContent.spec.js
@@ -1,32 +1,107 @@
-import { test } from '~util/testing'
-import { mount } from 'avoriaz'
+import { test } from '@util/testing'
import VExpansionPanelContent from './VExpansionPanelContent'
-// TODO: Fix when Vue has optional injects
-test.skip('VExpansionPanelContent.js', () => {
+const registrableWarning = '[Vuetify] The v-expansion-panel component must be used inside a v-expansion-panel-content'
+
+test('VExpansionPanelContent.js', ({ mount, compileToFunctions }) => {
it('should render component and match snapshot', () => {
- const wrapper = mount(VExpansionPanelContent)
+ const wrapper = mount(VExpansionPanelContent, {
+ slots: {
+ actions: [compileToFunctions('actions ')],
+ default: [compileToFunctions('default ')],
+ header: [compileToFunctions('header ')]
+ },
+ provide: {
+ focusable: true,
+ panelClick: jest.fn()
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(registrableWarning).toHaveBeenTipped()
+ })
+
+ it('should respect hideActions prop', () => {
+ const wrapper = mount(VExpansionPanelContent, {
+ propsData: {
+ hideActions: true
+ },
+ slots: {
+ actions: [compileToFunctions('actions ')],
+ header: [compileToFunctions('header ')]
+ },
+ provide: {
+ focusable: true,
+ panelClick: jest.fn()
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(registrableWarning).toHaveBeenTipped()
+ })
+
+ it('should render proper expand-icon', () => {
+ const wrapper = mount(VExpansionPanelContent, {
+ propsData: {
+ expandIcon: 'block'
+ },
+ slots: {
+ header: [compileToFunctions('header ')]
+ },
+ provide: {
+ focusable: true,
+ panelClick: jest.fn()
+ }
+ })
+
+ expect(wrapper.find('.icon')[0].element.textContent).toBe('block')
+ expect(registrableWarning).toHaveBeenTipped()
+ })
+
+ it('should toggle panel on header click', async () => {
+ const wrapper = mount(VExpansionPanelContent, {
+ slots: {
+ header: [compileToFunctions('header ')]
+ },
+ provide: {
+ focusable: true,
+ panelClick: uid => wrapper.vm.toggle(uid)
+ }
+ })
+ wrapper.find('.expansion-panel__header')[0].trigger('click')
+ await wrapper.vm.$nextTick()
expect(wrapper.html()).toMatchSnapshot()
+ expect(registrableWarning).toHaveBeenTipped()
})
it('should render an expanded component and match snapshot', () => {
const wrapper = mount(VExpansionPanelContent, {
propsData: {
ripple: true
+ },
+ provide: {
+ focusable: true,
+ panelClick: jest.fn()
}
})
expect(wrapper.html()).toMatchSnapshot()
+ expect(registrableWarning).toHaveBeenTipped()
})
it('should render an expanded component with lazy prop and match snapshot', () => {
const wrapper = mount(VExpansionPanelContent, {
propsData: {
lazy: true
+ },
+ provide: {
+ focusable: true,
+ panelClick: jest.fn()
}
})
expect(wrapper.html()).toMatchSnapshot()
+ expect(registrableWarning).toHaveBeenTipped()
})
})
diff --git a/src/components/VExpansionPanel/__snapshots__/VExpansionPanel.spec.js.snap b/src/components/VExpansionPanel/__snapshots__/VExpansionPanel.spec.js.snap
new file mode 100755
index 000000000000..09f1d9abee7e
--- /dev/null
+++ b/src/components/VExpansionPanel/__snapshots__/VExpansionPanel.spec.js.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VExpansionPanel.js should render an expanded component and match snapshot 1`] = `
+
+
+
+`;
+
+exports[`VExpansionPanel.js should render an expanded component and match snapshot 2`] = `
+
+
+
+`;
+
+exports[`VExpansionPanel.js should render component and match snapshot 1`] = `
+
+
+
+`;
+
+exports[`VExpansionPanel.js should render component and match snapshot 2`] = `
+
+
+
+`;
diff --git a/src/components/VExpansionPanel/__snapshots__/VExpansionPanelContent.spec.js.snap b/src/components/VExpansionPanel/__snapshots__/VExpansionPanelContent.spec.js.snap
new file mode 100755
index 000000000000..22b3760cb557
--- /dev/null
+++ b/src/components/VExpansionPanel/__snapshots__/VExpansionPanelContent.spec.js.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VExpansionPanelContent.js should render an expanded component and match snapshot 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`VExpansionPanelContent.js should render an expanded component with lazy prop and match snapshot 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`VExpansionPanelContent.js should render component and match snapshot 1`] = `
+
+
+
+
+
+ default
+
+
+
+
+`;
+
+exports[`VExpansionPanelContent.js should respect hideActions prop 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`VExpansionPanelContent.js should toggle panel on header click 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/components/VFooter/VFooter.js b/src/components/VFooter/VFooter.js
index 9e5721bf4ebf..01e2a02d3ef5 100644
--- a/src/components/VFooter/VFooter.js
+++ b/src/components/VFooter/VFooter.js
@@ -1,5 +1,7 @@
-require('../../stylus/components/_footer.styl')
+// Styles
+import '../../stylus/components/_footer.styl'
+// Mixins
import Applicationable from '../../mixins/applicationable'
import Colorable from '../../mixins/colorable'
import Themeable from '../../mixins/themeable'
@@ -7,59 +9,83 @@ import Themeable from '../../mixins/themeable'
export default {
name: 'v-footer',
- mixins: [Applicationable, Colorable, Themeable],
+ mixins: [
+ Applicationable('footer', [
+ 'height'
+ ]),
+ Colorable,
+ Themeable
+ ],
props: {
- absolute: Boolean,
- fixed: Boolean
+ height: {
+ default: 32,
+ type: [Number, String]
+ },
+ inset: Boolean
},
computed: {
- paddingLeft () {
- return this.fixed || !this.app
+ computedMarginBottom () {
+ if (!this.app) return
+
+ return this.$vuetify.application.bottom
+ },
+ computedPaddingLeft () {
+ return !this.app || !this.inset
? 0
: this.$vuetify.application.left
},
- paddingRight () {
- return this.fixed || !this.app
+ computedPaddingRight () {
+ return !this.app
? 0
: this.$vuetify.application.right
- }
- },
+ },
+ styles () {
+ const styles = {
+ height: isNaN(this.height) ? this.height : `${this.height}px`
+ }
- destroyed () {
- if (this.app) this.$vuetify.application.bottom = 0
- },
+ if (this.computedPaddingLeft) {
+ styles.paddingLeft = `${this.computedPaddingLeft}px`
+ }
- methods: {
- updateApplication () {
- if (!this.app) return
+ if (this.computedPaddingRight) {
+ styles.paddingRight = `${this.computedPaddingRight}px`
+ }
+
+ if (this.computedMarginBottom) {
+ styles.marginBottom = `${this.computedMarginBottom}px`
+ }
- this.$vuetify.application.bottom = this.fixed
- ? this.$el && this.$el.clientHeight
- : 0
+ return styles
}
},
- mounted () {
- this.updateApplication()
+ methods: {
+ /**
+ * Update the application layout
+ *
+ * @return {number}
+ */
+ updateApplication () {
+ return isNaN(this.height)
+ ? this.$el.clientHeight
+ : this.height
+ }
},
render (h) {
- this.updateApplication()
-
const data = {
staticClass: 'footer',
'class': this.addBackgroundColorClassChecks({
'footer--absolute': this.absolute,
- 'footer--fixed': this.fixed,
+ 'footer--fixed': !this.absolute && (this.app || this.fixed),
+ 'footer--inset': this.inset,
'theme--dark': this.dark,
'theme--light': this.light
}),
- style: {
- paddingLeft: `${this.paddingLeft}px`,
- paddingRight: `${this.paddingRight}px`
- },
+ style: this.styles,
ref: 'content'
}
diff --git a/src/components/VFooter/VFooter.spec.js b/src/components/VFooter/VFooter.spec.js
index 3b080a455d41..fbce9b7b8041 100644
--- a/src/components/VFooter/VFooter.spec.js
+++ b/src/components/VFooter/VFooter.spec.js
@@ -1,8 +1,7 @@
-import { test, functionalContext } from '~util/testing'
-import { mount } from 'avoriaz'
+import { test } from '@util/testing'
import VFooter from './VFooter'
-test('VFooter.js', () => {
+test('VFooter.js', ({ mount, functionalContext }) => {
it('should render component and match snapshot', () => {
const wrapper = mount(VFooter)
@@ -49,6 +48,62 @@ test('VFooter.js', () => {
})
expect(wrapper.element.classList).toContain('footer--absolute')
+ wrapper.setProps({ absolute: false })
expect(wrapper.element.classList).toContain('footer--fixed')
})
+
+ it('should get the right padding with app prop', async () => {
+ const wrapper = mount(VFooter, {
+ propsData: {
+ absolute: true,
+ app: true
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.vm.$vuetify.application.left = 20
+ wrapper.vm.$vuetify.application.right = 30
+ await wrapper.vm.$nextTick()
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should have margin bottom', async () => {
+ const wrapper = mount(VFooter, {
+ propsData: {
+ app: true,
+ height: 60
+ }
+ })
+
+ expect(wrapper.vm.$vuetify.application.footer).toBe(60)
+ wrapper.vm.$vuetify.application.bottom = 30
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.$vuetify.application.bottom).toBe(30)
+ })
+
+ it('should have padding left when using inset', async () => {
+ const wrapper = mount(VFooter, {
+ propsData: {
+ app: true,
+ inset: true
+ }
+ })
+
+ wrapper.vm.$vuetify.application.left = 300
+
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.computedPaddingLeft).toBe(300)
+ })
+
+ it('should accept an auto height', async () => {
+ const wrapper = mount(VFooter, {
+ attachToDocument: true,
+ propsData: {
+ height: 'auto'
+ }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
})
diff --git a/src/components/VFooter/__snapshots__/VFooter.spec.js.snap b/src/components/VFooter/__snapshots__/VFooter.spec.js.snap
new file mode 100755
index 000000000000..1cb235c6eedd
--- /dev/null
+++ b/src/components/VFooter/__snapshots__/VFooter.spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VFooter.js should accept an auto height 1`] = `
+
+
+
+`;
+
+exports[`VFooter.js should get the right padding with app prop 1`] = `
+
+
+
+`;
+
+exports[`VFooter.js should get the right padding with app prop 2`] = `
+
+
+
+`;
diff --git a/src/components/VForm/VForm.js b/src/components/VForm/VForm.js
index f141d6f62b31..b26e71c8e21d 100644
--- a/src/components/VForm/VForm.js
+++ b/src/components/VForm/VForm.js
@@ -33,7 +33,8 @@ export default {
const results = []
const search = (children, depth = 0) => {
- for (const child of children) {
+ for (let index = 0; index < children.length; index++) {
+ const child = children[index]
if (child.errorBucket !== undefined) {
results.push(child)
} else {
@@ -46,7 +47,8 @@ export default {
return search(this.$children)
},
watchInputs (inputs = this.getInputs()) {
- for (const child of inputs) {
+ for (let index = 0; index < inputs.length; index++) {
+ const child = inputs[index]
if (this.inputs.includes(child)) {
continue // We already know about this input
}
@@ -56,8 +58,8 @@ export default {
}
},
watchChild (child) {
- const watcher = (child) => {
- child.$watch('valid', (val) => {
+ const watcher = child => {
+ child.$watch('valid', val => {
this.$set(this.errorBag, child._uid, !val)
}, { immediate: true })
}
@@ -65,7 +67,7 @@ export default {
if (!this.lazyValidation) return watcher(child)
// Only start watching inputs if we need to
- child.$watch('shouldValidate', (val) => {
+ child.$watch('shouldValidate', val => {
if (!val) return
// Only watch if we're not already doing it
@@ -79,10 +81,10 @@ export default {
return !errors
},
reset () {
- this.inputs.forEach((input) => input.reset())
- if (this.lazyValidation) {
- Object.keys(this.errorBag).forEach(key => this.$delete(this.errorBag, key))
+ for (let i = this.inputs.length; i--;) {
+ this.inputs[i].reset()
}
+ if (this.lazyValidation) this.errorBag = {}
}
},
@@ -97,7 +99,8 @@ export default {
// Something was removed, we don't want it in the errorBag any more
const removed = this.inputs.filter(i => !inputs.includes(i))
- for (const input of removed) {
+ for (let index = 0; index < removed.length; index++) {
+ const input = removed[index]
this.$delete(this.errorBag, input._uid)
this.$delete(this.inputs, this.inputs.indexOf(input))
}
diff --git a/src/components/VForm/VForm.spec.js b/src/components/VForm/VForm.spec.js
index ced3e78e91f3..8a87205d580f 100644
--- a/src/components/VForm/VForm.spec.js
+++ b/src/components/VForm/VForm.spec.js
@@ -1,8 +1,7 @@
import Vue from 'vue'
-import { mount } from 'avoriaz'
-import { test } from '~util/testing'
-import VTextField from '~components/VTextField'
-import VBtn from '~components/VBtn'
+import { test } from '@util/testing'
+import VTextField from '@components/VTextField'
+import VBtn from '@components/VBtn'
import VForm from './VForm'
const inputOne = Vue.component('input-one', {
@@ -13,7 +12,7 @@ const inputOne = Vue.component('input-one', {
}
})
-test('VForm.js', () => {
+test('VForm.js', ({ mount }) => {
it('should pass on listeners to form element', async () => {
const submit = jest.fn()
const component = Vue.component('test', {
@@ -41,4 +40,119 @@ test('VForm.js', () => {
expect(submit).toBeCalled()
})
+
+ it('should watch the error bag', async () => {
+ const wrapper = mount(VForm)
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+
+ Vue.set(wrapper.vm.errorBag, 'foo', true)
+ await Vue.nextTick()
+ expect(input).toBeCalledWith(false)
+
+ Vue.set(wrapper.vm.errorBag, 'foo', false)
+ await Vue.nextTick()
+ expect(input).toBeCalledWith(true)
+ })
+
+ it('should register input child', async () => {
+ const wrapper = mount(VForm, {
+ slots: {
+ default: [VTextField]
+ }
+ })
+
+ expect(wrapper.vm.inputs.length).toBe(1)
+ wrapper.vm.watchInputs()
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.inputs.length).toBe(1)
+ })
+
+ it('should only watch children if not lazy', async () => {
+ const wrapper = mount(VForm, {
+ propsData: {
+ lazyValidation: true
+ },
+ slots: {
+ default: [VTextField]
+ }
+ })
+
+ const input = wrapper.vm.getInputs()[0]
+ wrapper.vm.watchChild(input)
+ input.shouldValidate = true
+ wrapper.vm.watchChild(input)
+ await wrapper.vm.$nextTick()
+
+ // beware, depends on number of computeds in VTextField
+ const watchers = 28
+ expect(input._watchers.length).toBe(watchers)
+ input.shouldValidate = false
+ wrapper.vm.watchChild(input)
+ await wrapper.vm.$nextTick()
+
+ expect(input._watchers.length).toBe(watchers + 1)
+ input.shouldValidate = true
+ await wrapper.vm.$nextTick()
+
+ expect(Object.keys(wrapper.vm.errorBag).length).toBe(1)
+ })
+
+ it('should validate all inputs', async () => {
+ const wrapper = mount(VForm, {
+ slots: {
+ default: [{
+ render (h) {
+ return h(VTextField, {
+ props: {
+ rules: [v => v === 1 || 'Error']
+ }
+ })
+ }
+ }]
+ }
+ })
+
+ expect(wrapper.vm.validate()).toBe(false)
+ })
+
+ it('should reset all inputs', async () => {
+ const wrapper = mount(VForm, {
+ slots: {
+ default: [VTextField]
+ }
+ })
+
+ const event = jest.fn()
+ const input = wrapper.find(VTextField)[0]
+ input.vm.$on('input', event)
+
+ expect(Object.keys(wrapper.vm.errorBag).length).toBe(1)
+ wrapper.vm.reset()
+
+ expect(Object.keys(wrapper.vm.errorBag).length).toBe(1)
+ expect(event).toHaveBeenCalledWith(null)
+
+ wrapper.setProps({ lazyValidation: true })
+ expect(Object.keys(wrapper.vm.errorBag).length).toBe(1)
+
+ wrapper.vm.reset()
+ expect(Object.keys(wrapper.vm.errorBag).length).toBe(0)
+ })
+
+ it('should update inputs when updated lifecycle hook is called', async () => {
+ const wrapper = mount(VForm, {
+ slots: {
+ default: [VTextField]
+ }
+ })
+
+ const input = wrapper.find(VTextField)[0]
+ expect(wrapper.vm.inputs.length).toBe(1)
+ input.vm.$destroy()
+ wrapper.update()
+ await wrapper.vm.$nextTick()
+ expect(wrapper.vm.inputs.length).toBe(0)
+ })
})
diff --git a/src/components/VGrid/VContainer.js b/src/components/VGrid/VContainer.js
index 1e57818b3b7f..0df99f720934 100644
--- a/src/components/VGrid/VContainer.js
+++ b/src/components/VGrid/VContainer.js
@@ -1,4 +1,4 @@
-require('../../stylus/components/_grid.styl')
+import '../../stylus/components/_grid.styl'
import Grid from './grid'
diff --git a/src/components/VGrid/VContent.js b/src/components/VGrid/VContent.js
index b5a9b137625c..f4755051c4ab 100644
--- a/src/components/VGrid/VContent.js
+++ b/src/components/VGrid/VContent.js
@@ -1,8 +1,14 @@
-require('../../stylus/components/_content.styl')
+// Styles
+import '../../stylus/components/_content.styl'
+
+// Mixins
+import SSRBootable from '../../mixins/ssr-bootable'
export default {
name: 'v-content',
+ mixins: [SSRBootable],
+
props: {
tag: {
type: String,
@@ -13,36 +19,32 @@ export default {
computed: {
styles () {
const {
- bar, top, right, bottom, left
+ bar, top, right, footer, bottom, left
} = this.$vuetify.application
return {
paddingTop: `${top + bar}px`,
paddingRight: `${right}px`,
- paddingBottom: `${bottom}px`,
+ paddingBottom: `${footer + bottom}px`,
paddingLeft: `${left}px`
}
}
},
- mounted () {
- // TODO: Deprecate
- if (this.$el.parentElement.tagName === 'MAIN') {
- console.warn('v-content no longer needs to be wrapped in a tag', this.$el.parentElement)
- }
- },
-
render (h) {
const data = {
staticClass: 'content',
+ 'class': this.classes,
style: this.styles,
ref: 'content'
}
- return h('div', {
- staticClass: 'content--wrap'
- }, [
- h(this.tag, data, this.$slots.default)
+ return h(this.tag, data, [
+ h(
+ 'div',
+ { staticClass: 'content--wrap' },
+ this.$slots.default
+ )
])
}
}
diff --git a/src/components/VGrid/VFlex.js b/src/components/VGrid/VFlex.js
index ba112938daeb..7a05762de508 100644
--- a/src/components/VGrid/VFlex.js
+++ b/src/components/VGrid/VFlex.js
@@ -1,4 +1,4 @@
-require('../../stylus/components/_grid.styl')
+import '../../stylus/components/_grid.styl'
import Grid from './grid'
diff --git a/src/components/VGrid/VGrid.spec.js b/src/components/VGrid/VGrid.spec.js
index 588ce89214a9..4663aa3633f5 100644
--- a/src/components/VGrid/VGrid.spec.js
+++ b/src/components/VGrid/VGrid.spec.js
@@ -1,14 +1,28 @@
-import { test } from '~util/testing'
-import VFlex from '~components/VGrid/VFlex'
+import { test } from '@util/testing'
+import VFlex from '@components/VGrid/VFlex'
test('VFlex', ({ mount, functionalContext }) => {
it('should conditionally apply if boolean is used', () => {
const wrapper = mount(VFlex, functionalContext({
attrs: {
- md6: false
+ foo: '',
+ bar: false
}
}))
- expect(wrapper.hasClass('md6')).toBe(false)
+ expect(wrapper.hasAttribute('foo')).toBe(false)
+ expect(wrapper.hasAttribute('bar')).toBe(false)
+ expect(wrapper.hasClass('foo')).toBe(true)
+ expect(wrapper.hasClass('bar')).toBe(false)
+ })
+
+ it('should pass the id attr', () => {
+ const wrapper = mount(VFlex, functionalContext({
+ attrs: {
+ id: 'test'
+ }
+ }))
+
+ expect(wrapper.find('#test')).toHaveLength(1)
})
})
diff --git a/src/components/VGrid/VLayout.js b/src/components/VGrid/VLayout.js
index 71ad01799d83..8cb49a6e88a4 100644
--- a/src/components/VGrid/VLayout.js
+++ b/src/components/VGrid/VLayout.js
@@ -1,4 +1,4 @@
-require('../../stylus/components/_grid.styl')
+import '../../stylus/components/_grid.styl'
import Grid from './grid'
diff --git a/src/components/VGrid/grid.js b/src/components/VGrid/grid.js
index b91ca652e5c0..34de16a3250e 100644
--- a/src/components/VGrid/grid.js
+++ b/src/components/VGrid/grid.js
@@ -16,13 +16,9 @@ export default function Grid (name) {
data.staticClass = (`${name} ${data.staticClass || ''}`).trim()
if (data.attrs) {
- const classes = []
-
- Object.keys(data.attrs).forEach(key => {
+ const classes = Object.keys(data.attrs).filter(key => {
const value = data.attrs[key]
-
- if (typeof value === 'string') classes.push(key)
- else if (value) classes.push(key)
+ return value || typeof value === 'string'
})
if (classes.length) data.staticClass += ` ${classes.join(' ')}`
diff --git a/src/components/VIcon/VIcon.js b/src/components/VIcon/VIcon.js
index 130246471260..5fe95e2983f4 100644
--- a/src/components/VIcon/VIcon.js
+++ b/src/components/VIcon/VIcon.js
@@ -1,8 +1,20 @@
-require('../../stylus/components/_icons.styl')
+import '../../stylus/components/_icons.styl'
import Themeable from '../../mixins/themeable'
import Colorable from '../../mixins/colorable'
+const SIZE_MAP = {
+ small: '16px',
+ default: '24px',
+ medium: '28px',
+ large: '36px',
+ xLarge: '40px'
+}
+
+function isFontAwesome5 (iconType) {
+ return ['fas', 'far', 'fal', 'fab'].some(val => iconType.includes(val))
+}
+
export default {
name: 'v-icon',
@@ -16,55 +28,74 @@ export default {
left: Boolean,
medium: Boolean,
right: Boolean,
+ size: {
+ type: [Number, String]
+ },
+ small: Boolean,
xLarge: Boolean
},
render (h, { props, data, children = [] }) {
+ const { small, medium, large, xLarge } = props
+ const sizes = { small, medium, large, xLarge }
+ const explicitSize = Object.keys(sizes).find(key => sizes[key] && key)
+ const fontSize = (explicitSize && SIZE_MAP[explicitSize]) || props.size
+
+ if (fontSize) data.style = { fontSize, ...data.style }
+
let iconName = ''
- if (children.length) {
- iconName = children.pop().text
- } else if (data.domProps && data.domProps.textContent) {
- iconName = data.domProps.textContent
+ if (children.length) iconName = children.pop().text
+ // Support usage of v-text and v-html
+ else if (data.domProps) {
+ iconName = data.domProps.textContent ||
+ data.domProps.innerHTML ||
+ iconName
+
+ // Remove nodes so it doesn't
+ // overwrite our changes
delete data.domProps.textContent
- } else if (data.domProps && data.domProps.innerHTML) {
- iconName = data.domProps.innerHTML
delete data.domProps.innerHTML
}
let iconType = 'material-icons'
- const thirdPartyIcon = iconName.indexOf('-') > -1
- if (thirdPartyIcon) iconType = iconName.slice(0, iconName.indexOf('-'))
+ // Material Icon delimiter is _
+ // https://material.io/icons/
+ const delimiterIndex = iconName.indexOf('-')
+ const isCustomIcon = delimiterIndex > -1
- data.staticClass = (`${iconType} icon ${data.staticClass || ''}`).trim()
- data.attrs = data.attrs || {}
+ if (isCustomIcon) {
+ iconType = iconName.slice(0, delimiterIndex)
+
+ if (isFontAwesome5(iconType)) iconType = ''
+ // Assume if not a custom icon
+ // is Material Icon font
+ } else children.push(iconName)
+ data.attrs = data.attrs || {}
if (!('aria-hidden' in data.attrs)) {
data.attrs['aria-hidden'] = true
}
const classes = Object.assign({
'icon--disabled': props.disabled,
- 'icon--large': props.large,
'icon--left': props.left,
- 'icon--medium': props.medium,
'icon--right': props.right,
- 'icon--x-large': props.xLarge,
'theme--dark': props.dark,
'theme--light': props.light
- }, props.color ? Colorable.methods.addTextColorClassChecks.call(props, {}, 'color') : {
- 'primary--text': props.primary,
- 'secondary--text': props.secondary,
- 'success--text': props.success,
- 'info--text': props.info,
- 'warning--text': props.warning,
- 'error--text': props.error
- })
-
- const iconClasses = Object.keys(classes).filter(k => classes[k]).join(' ')
- iconClasses && (data.staticClass += ` ${iconClasses}`)
-
- if (thirdPartyIcon) data.staticClass += ` ${iconName}`
- else children.push(iconName)
+ }, props.color ? Colorable.methods.addTextColorClassChecks.call(props, {}, props.color) : {})
+
+ // Order classes
+ // * Component class
+ // * Vuetify classes
+ // * Icon Classes
+ data.staticClass = [
+ 'icon',
+ data.staticClass,
+ Object.keys(classes).filter(k => classes[k]).join(' '),
+ iconType,
+ isCustomIcon ? iconName : null
+ ].reduce((prev, curr) => curr ? `${prev} ${curr}` : prev)
+ .trim()
return h('i', data, children)
}
diff --git a/src/components/VIcon/VIcon.spec.js b/src/components/VIcon/VIcon.spec.js
index bae18ff9db01..bf2ed3bba276 100644
--- a/src/components/VIcon/VIcon.spec.js
+++ b/src/components/VIcon/VIcon.spec.js
@@ -1,14 +1,13 @@
-import VIcon from '~components/VIcon'
-import { test, functionalContext } from '~util/testing'
-import { mount } from 'avoriaz'
+import VIcon from '@components/VIcon'
+import { test, functionalContext } from '@util/testing'
-test('VIcon.js', () => {
+test('VIcon.js', ({ mount, compileToFunctions }) => {
it('should render component', () => {
const context = functionalContext({}, 'add')
const wrapper = mount(VIcon, context)
expect(wrapper.text()).toBe('add')
- expect(wrapper.element.className).toBe('material-icons icon')
+ expect(wrapper.element.className).toBe('icon material-icons')
})
it('should render a colored component', () => {
@@ -26,25 +25,34 @@ test('VIcon.js', () => {
expect(wrapper.element.classList).toContain('icon--disabled')
})
- it('should render a large size component', () => {
- const context = functionalContext({ props: { large: true } }, 'add')
+ it('should not set font size if none provided', () => {
+ const context = functionalContext({}, 'add')
const wrapper = mount(VIcon, context)
- expect(wrapper.element.classList).toContain('icon--large')
+ expect(wrapper.element.style.fontSize).toBe('')
})
- it('should render a medium size component', () => {
- const context = functionalContext({ props: { medium: true } }, 'add')
- const wrapper = mount(VIcon, context)
+ it('should render a mapped size', () => {
+ const SIZE_MAP = {
+ small: '16px',
+ medium: '28px',
+ large: '36px',
+ xLarge: '40px'
+ }
+
+ Object.keys(SIZE_MAP).forEach(size => {
+ const context = functionalContext({ props: { [size]: true } }, 'add')
+ const wrapper = mount(VIcon, context)
- expect(wrapper.element.classList).toContain('icon--medium')
+ expect(wrapper.element.style.fontSize).toBe(SIZE_MAP[size])
+ })
})
- it('should render a xLarge size component', () => {
- const context = functionalContext({ props: { xLarge: true } }, 'add')
+ it('should render a specific size', () => {
+ const context = functionalContext({ props: { size: '112px' } }, 'add')
const wrapper = mount(VIcon, context)
- expect(wrapper.element.classList).toContain('icon--x-large')
+ expect(wrapper.element.style.fontSize).toBe('112px')
})
it('should render a left aligned component', () => {
@@ -61,12 +69,27 @@ test('VIcon.js', () => {
expect(wrapper.element.classList).toContain('icon--right')
})
+ it('should render a component with aria-hidden attr', () => {
+ const context = functionalContext({ attrs: { 'aria-hidden': 'foo' } }, 'add')
+ const wrapper = mount(VIcon, context)
+
+ expect(wrapper.element.getAttribute('aria-hidden')).toBe('foo')
+ })
+
it('should allow third-party icons when using - prefix', () => {
const context = functionalContext({ props: {} }, 'fa-add')
const wrapper = mount(VIcon, context)
expect(wrapper.text()).toBe('')
- expect(wrapper.element.className).toBe('fa icon fa-add')
+ expect(wrapper.element.className).toBe('icon fa fa-add')
+ })
+
+ it('should support font awesome 5 icons when using - prefix', () => {
+ const context = functionalContext({ props: {} }, 'fab fa-facebook')
+ const wrapper = mount(VIcon, context)
+
+ expect(wrapper.text()).toBe('')
+ expect(wrapper.element.className).toBe('icon fab fa-facebook')
})
it('should allow the use of v-text', () => {
@@ -75,7 +98,7 @@ test('VIcon.js', () => {
}))
expect(wrapper.text()).toBe('')
- expect(wrapper.element.className).toBe('fa icon fa-home')
+ expect(wrapper.element.className).toBe('icon fa fa-home')
})
it('should allow the use of v-html', () => {
@@ -84,6 +107,37 @@ test('VIcon.js', () => {
}))
expect(wrapper.text()).toBe('')
- expect(wrapper.element.className).toBe('fa icon fa-home')
+ expect(wrapper.element.className).toBe('icon fa fa-home')
+ })
+
+ it('set font size from helper prop', async () => {
+ const iconFactory = size => mount(VIcon, functionalContext({
+ props: { [size]: true }
+ }))
+
+ const small = iconFactory('small')
+ expect(small.html()).toMatchSnapshot()
+
+ const medium = iconFactory('medium')
+ expect(medium.html()).toMatchSnapshot()
+
+ const large = iconFactory('large')
+ expect(large.html()).toMatchSnapshot()
+
+ const xLarge = iconFactory('xLarge')
+ expect(xLarge.html()).toMatchSnapshot()
+ })
+
+ it('should have proper classname', () => {
+ const wrapper = mount(VIcon, functionalContext({
+ props: {
+ color: 'primary'
+ },
+ domProps: {
+ innerHTML: 'fa-lock'
+ }
+ }))
+
+ expect(wrapper.element.className).toBe('icon primary--text fa fa-lock')
})
})
diff --git a/src/components/VIcon/__snapshots__/VIcon.spec.js.snap b/src/components/VIcon/__snapshots__/VIcon.spec.js.snap
new file mode 100644
index 000000000000..7594b6eb969e
--- /dev/null
+++ b/src/components/VIcon/__snapshots__/VIcon.spec.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VIcon.js set font size from helper prop 1`] = `
+
+
+
+
+`;
+
+exports[`VIcon.js set font size from helper prop 2`] = `
+
+
+
+
+`;
+
+exports[`VIcon.js set font size from helper prop 3`] = `
+
+
+
+
+`;
+
+exports[`VIcon.js set font size from helper prop 4`] = `
+
+
+
+
+`;
diff --git a/src/components/VJumbotron/VJumbotron.js b/src/components/VJumbotron/VJumbotron.js
new file mode 100644
index 000000000000..60dbd41edbe7
--- /dev/null
+++ b/src/components/VJumbotron/VJumbotron.js
@@ -0,0 +1,93 @@
+import '../../stylus/components/_jumbotrons.styl'
+
+// Mixins
+import Colorable from '../../mixins/colorable'
+import Routable from '../../mixins/routable'
+import Themeable from '../../mixins/themeable'
+
+export default {
+ name: 'v-jumbotron',
+
+ mixins: [
+ Colorable,
+ Routable,
+ Themeable
+ ],
+
+ props: {
+ gradient: String,
+ height: {
+ type: [Number, String],
+ default: '400px'
+ },
+ src: String,
+ tag: {
+ type: String,
+ default: 'div'
+ }
+ },
+
+ computed: {
+ backgroundStyles () {
+ const styles = {}
+
+ if (this.gradient) {
+ styles.background = `linear-gradient(${this.gradient})`
+ }
+
+ return styles
+ },
+ classes () {
+ return {
+ 'theme--dark': this.dark,
+ 'theme--light': this.light
+ }
+ },
+ styles () {
+ return {
+ height: this.height
+ }
+ }
+ },
+
+ methods: {
+ genBackground () {
+ return this.$createElement('div', {
+ staticClass: 'jumbotron__background',
+ 'class': this.addBackgroundColorClassChecks(),
+ style: this.backgroundStyles
+ })
+ },
+ genContent () {
+ return this.$createElement('div', {
+ staticClass: 'jumbotron__content'
+ }, this.$slots.default)
+ },
+ genImage () {
+ if (!this.src) return null
+ if (this.$slots.img) return this.$slots.img({ src: this.src })
+
+ return this.$createElement('img', {
+ staticClass: 'jumbotron__image',
+ attrs: { src: this.src }
+ })
+ },
+ genWrapper () {
+ return this.$createElement('div', {
+ staticClass: 'jumbotron__wrapper'
+ }, [
+ this.genImage(),
+ this.genBackground(),
+ this.genContent()
+ ])
+ }
+ },
+
+ render (h) {
+ const { tag, data } = this.generateRouteLink()
+ data.staticClass = 'jumbotron'
+ data.style = this.styles
+
+ return h(tag, data, [this.genWrapper()])
+ }
+}
diff --git a/src/components/VJumbotron/index.js b/src/components/VJumbotron/index.js
new file mode 100644
index 000000000000..8dbce5018b16
--- /dev/null
+++ b/src/components/VJumbotron/index.js
@@ -0,0 +1,8 @@
+import VJumbotron from './VJumbotron'
+
+/* istanbul ignore next */
+VJumbotron.install = function install (Vue) {
+ Vue.component(VJumbotron.name, VJumbotron)
+}
+
+export default VJumbotron
diff --git a/src/components/VList/VList.js b/src/components/VList/VList.js
index 80c70b3ec4bd..67873d8a9762 100644
--- a/src/components/VList/VList.js
+++ b/src/components/VList/VList.js
@@ -1,28 +1,33 @@
-require('../../stylus/components/_lists.styl')
+// Styles
+import '../../stylus/components/_lists.styl'
+// Mixins
import Themeable from '../../mixins/themeable'
+import {
+ provide as RegistrableProvide
+} from '../../mixins/registrable'
export default {
name: 'v-list',
+ mixins: [
+ RegistrableProvide('list'),
+ Themeable
+ ],
+
provide () {
return {
- listClick: this.listClick,
- listClose: this.listClose
+ 'listClick': this.listClick
}
},
- mixins: [Themeable],
-
- data () {
- return {
- uid: null,
- groups: []
- }
- },
+ data: () => ({
+ groups: []
+ }),
props: {
dense: Boolean,
+ expand: Boolean,
subheader: Boolean,
threeLine: Boolean,
twoLine: Boolean
@@ -31,43 +36,40 @@ export default {
computed: {
classes () {
return {
- 'list': true,
- 'list--two-line': this.twoLine,
'list--dense': this.dense,
- 'list--three-line': this.threeLine,
'list--subheader': this.subheader,
- 'theme--dark dark--bg': this.dark,
- 'theme--light light--bg': this.light
+ 'list--two-line': this.twoLine,
+ 'list--three-line': this.threeLine,
+ 'theme--dark': this.dark,
+ 'theme--light': this.light
}
}
},
- watch: {
- uid () {
- this.$children.filter(i => i.$options._componentTag === 'v-list-group').forEach(i => i.toggle(this.uid))
- }
- },
-
methods: {
- listClick (uid, force) {
- if (force) {
- this.uid = uid
- } else {
- this.uid = this.uid === uid ? null : uid
+ register (uid, cb) {
+ this.groups.push({ uid, cb })
+ },
+ unregister (uid) {
+ const index = this.groups.findIndex(g => g.uid === uid)
+
+ if (index > -1) {
+ this.groups.splice(index, 1)
}
},
+ listClick (uid, isBooted) {
+ if (this.expand) return
- listClose (uid) {
- if (this.uid === uid) {
- this.uid = null
+ for (let i = this.groups.length; i--;) {
+ this.groups[i].cb(uid)
}
}
},
render (h) {
const data = {
- 'class': this.classes,
- attrs: { 'data-uid': this._uid }
+ staticClass: 'list',
+ 'class': this.classes
}
return h('ul', data, [this.$slots.default])
diff --git a/src/components/VList/VList.spec.js b/src/components/VList/VList.spec.js
index bb044d9e5b90..4acd1ad0bed7 100644
--- a/src/components/VList/VList.spec.js
+++ b/src/components/VList/VList.spec.js
@@ -1,5 +1,5 @@
-import VList from '~components/VList'
-import { test } from '~util/testing'
+import VList from '@components/VList'
+import { test } from '@util/testing'
// TODO: Test actual behaviour instead of classes
test('VList.js', ({ mount }) => {
diff --git a/src/components/VList/VListGroup.js b/src/components/VList/VListGroup.js
index 3e7f73c675df..04c26c687956 100644
--- a/src/components/VList/VListGroup.js
+++ b/src/components/VList/VListGroup.js
@@ -1,36 +1,76 @@
-import { VExpandTransition } from '../transitions'
+// Components
+import VIcon from '../../components/VIcon'
+// Mixins
import Bootable from '../../mixins/bootable'
import Toggleable from '../../mixins/toggleable'
+import {
+ inject as RegistrableInject
+} from '../../mixins/registrable'
+
+// Transitions
+import { VExpandTransition } from '../transitions'
+/**
+ * List group
+ *
+ * @component
+ */
export default {
name: 'v-list-group',
- inject: ['listClick', 'listClose'],
+ mixins: [
+ Bootable,
+ RegistrableInject('list', 'v-list-group', 'v-list'),
+ Toggleable
+ ],
+
+ inject: ['listClick'],
- mixins: [Bootable, Toggleable],
+ data: () => ({
+ groups: []
+ }),
props: {
+ activeClass: {
+ type: String,
+ default: 'primary--text'
+ },
+ appendIcon: {
+ type: String,
+ default: 'keyboard_arrow_down'
+ },
+ disabled: Boolean,
group: String,
- noAction: Boolean
+ noAction: Boolean,
+ prependIcon: String,
+ subGroup: Boolean
},
computed: {
- classes () {
+ groupClasses () {
return {
- 'list--group__header': true,
- 'list--group__header--active': this.isActive,
- 'list--group__header--no-action': this.noAction
+ 'list__group--active': this.isActive,
+ 'list__group--disabled': this.disabled
+ }
+ },
+ headerClasses () {
+ return {
+ 'list__group__header--active': this.isActive,
+ 'list__group__header--sub-group': this.subGroup
+ }
+ },
+ itemsClasses () {
+ return {
+ 'list__group__items--no-action': this.noAction
}
}
},
watch: {
- isActive () {
- this.isBooted = true
-
- if (!this.isActive) {
- this.listClose(this._uid)
+ isActive (val) {
+ if (!this.subGroup && val) {
+ this.listClick(this._uid)
}
},
$route (to) {
@@ -40,28 +80,89 @@ export default {
if (isActive && this.isActive !== isActive) {
this.listClick(this._uid)
}
+
this.isActive = isActive
}
}
},
mounted () {
- this.isBooted = this.isActive
+ this.list.register(this._uid, this.toggle)
- if (this.group) {
+ if (this.group &&
+ this.$route &&
+ this.value == null
+ ) {
this.isActive = this.matchRoute(this.$route.path)
}
+ },
- if (this.isActive) {
- this.listClick(this._uid)
- }
+ beforeDestroy () {
+ this.list.unregister(this._uid)
},
methods: {
click () {
- if (!this.$refs.item.querySelector('.list__tile--disabled')) {
- requestAnimationFrame(() => this.listClick(this._uid))
- }
+ if (this.disabled) return
+
+ this.isActive = !this.isActive
+ },
+ genIcon (icon) {
+ return this.$createElement(VIcon, icon)
+ },
+ genAppendIcon () {
+ const icon = !this.subGroup ? this.appendIcon : false
+
+ if (!icon && !this.$slots.appendIcon) return null
+
+ return this.$createElement('li', {
+ staticClass: 'list__group__header__append-icon'
+ }, [
+ this.$slots.appendIcon || this.genIcon(icon)
+ ])
+ },
+ genGroup () {
+ return this.$createElement('ul', {
+ staticClass: 'list__group__header',
+ 'class': this.headerClasses,
+ on: Object.assign({}, {
+ click: this.click
+ }, this.$listeners),
+ ref: 'item'
+ }, [
+ this.genPrependIcon(),
+ this.$slots.activator,
+ this.genAppendIcon()
+ ])
+ },
+ genItems () {
+ return this.$createElement('ul', {
+ staticClass: 'list__group__items',
+ 'class': this.itemsClasses,
+ directives: [{
+ name: 'show',
+ value: this.isActive
+ }],
+ ref: 'group'
+ }, this.showLazyContent(this.$slots.default))
+ },
+ genPrependIcon () {
+ const icon = this.prependIcon
+ ? this.prependIcon
+ : this.subGroup
+ ? 'arrow_drop_down'
+ : false
+
+ if (!icon && !this.$slots.prependIcon) return null
+
+ return this.$createElement('li', {
+ staticClass: 'list__group__header__prepend-icon',
+ 'class': {
+ [this.activeClass]: this.isActive
+ }
+ }, [
+ this.$slots.prependIcon || this.genIcon(icon)
+ ])
},
toggle (uid) {
this.isActive = this._uid === uid
@@ -73,23 +174,12 @@ export default {
},
render (h) {
- const group = h('ul', {
- 'class': 'list list--group',
- directives: [{
- name: 'show',
- value: this.isActive
- }],
- ref: 'group'
- }, this.showLazyContent(this.$slots.default))
-
- const item = h('ul', {
- 'class': this.classes,
- on: Object.assign({}, { click: this.click }, this.$listeners),
- ref: 'item'
- }, [this.$slots.item])
-
- const transition = h(VExpandTransition, [group])
-
- return h('li', { 'class': 'list--group__container' }, [item, transition])
+ return h('li', {
+ staticClass: 'list__group',
+ 'class': this.groupClasses
+ }, [
+ this.genGroup(),
+ h(VExpandTransition, [this.genItems()])
+ ])
}
}
diff --git a/src/components/VList/VListGroup.spec.js b/src/components/VList/VListGroup.spec.js
index e2b6e4c3aed0..00071ca41061 100644
--- a/src/components/VList/VListGroup.spec.js
+++ b/src/components/VList/VListGroup.spec.js
@@ -1,5 +1,7 @@
-import { VList, VListGroup } from '~components/VList'
-import { test } from '~util/testing'
+import { VList, VListGroup } from '@components/VList'
+import { test } from '@util/testing'
+
+const warning = '[Vuetify] The v-list-group component must be used inside a v-list'
// TODO: Test actual behaviour instead of classes
test('VListGroup.js', ({ mount }) => {
@@ -55,4 +57,167 @@ test('VListGroup.js', ({ mount }) => {
expect(wrapper.html()).toMatchSnapshot()
})
+
+ it('should toggle based upon matching uid', () => {
+ const listClick = jest.fn()
+ const wrapper = mount(VListGroup, {
+ provide: {
+ listClick: listClick,
+ list: {
+ register: () => {},
+ unregister: () => {}
+ }
+ }
+ })
+
+ expect(wrapper.vm.isActive).toBe(false)
+ wrapper.vm.toggle(wrapper.vm._uid)
+ expect(wrapper.vm.isActive).toBe(true)
+ wrapper.vm.toggle(null)
+ expect(wrapper.vm.isActive).toBe(false)
+ })
+
+ it('should accept a custom active class', () => {
+ const wrapper = mount(VListGroup, {
+ attachToDocument: true,
+ propsData: {
+ activeClass: 'foo',
+ prependIcon: 'list',
+ value: true
+ }
+ })
+
+ const header = wrapper.find('.list__group__header__prepend-icon')[0]
+
+ expect(header.hasClass('foo')).toBe(true)
+ wrapper.setProps({ activeClass: 'bar' })
+ expect(header.hasClass('bar')).toBe(true)
+
+ expect('Injection "listClick" not found').toHaveBeenWarned()
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should open if no value provided and group matches route', async () => {
+ const $route = { path: '/foo' }
+ const listClick = jest.fn()
+ const wrapper = mount(VListGroup, {
+ attachToDocument: true,
+ propsData: {
+ group: 'foo'
+ },
+ provide: {
+ listClick
+ },
+ globals: {
+ $route
+ }
+ })
+
+ await wrapper.vm.$nextTick()
+ expect(listClick).toBeCalledWith(wrapper.vm._uid)
+
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should toggle when clicked', async () => {
+ const wrapper = mount(VListGroup, {
+ attachToDocument: true,
+ provide: {
+ listClick: () => {}
+ }
+ })
+
+ const input = jest.fn()
+ wrapper.vm.$on('input', input)
+ wrapper.vm.click()
+ await wrapper.vm.$nextTick()
+ expect(input).toBeCalledWith(true)
+
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should unregister when destroyed', async () => {
+ const unregister = jest.fn()
+ const wrapper = mount(VListGroup, {
+ attachToDocument: true,
+ provide: {
+ listClick: () => {},
+ list: {
+ register: () => {},
+ unregister
+ }
+ }
+ })
+
+ wrapper.destroy()
+ await wrapper.vm.$nextTick()
+ expect(unregister).toBeCalledWith(wrapper.vm._uid)
+ })
+
+ it('should render a custom append icon', async () => {
+ const wrapper = mount(VListGroup, {
+ slots: {
+ appendIcon: {
+ render: h => h('span', 'bar')
+ }
+ }
+ })
+
+ const icon = wrapper.find('span')[0]
+ expect(icon.html()).toBe('bar ')
+
+ expect('Injection "listClick" not found').toHaveBeenWarned()
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should only render custom prepend icon', async () => {
+ const wrapper = mount(VListGroup, {
+ slots: {
+ prependIcon: {
+ render: h => h('span', 'bar')
+ }
+ }
+ })
+
+ const icon = wrapper.find('span')[0]
+ expect(icon.html()).toBe('bar ')
+
+ expect('Injection "listClick" not found').toHaveBeenWarned()
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should render a default prepended icon', async () => {
+ const wrapper = mount(VListGroup, {
+ propsData: {
+ subGroup: true
+ }
+ })
+
+ const icon = wrapper.find('.icon')[0]
+
+ expect(icon.text()).toBe('arrow_drop_down')
+
+ expect('Injection "listClick" not found').toHaveBeenWarned()
+ expect(warning).toHaveBeenTipped()
+ })
+
+ it('should return proper content from icon methods', () => {
+ const wrapper = mount(VListGroup)
+
+ expect(wrapper.vm.genPrependIcon()).toBe(null)
+
+ wrapper.setProps({ prependIcon: 'list' })
+
+ expect(wrapper.vm.genPrependIcon()).toBeTruthy()
+ wrapper.setProps({ prependIcon: undefined })
+
+ const icon = wrapper.find('.icon')[0]
+
+ expect(icon.text()).toBe('keyboard_arrow_down')
+ wrapper.setProps({ appendIcon: 'list' })
+
+ expect(icon.text()).toBe('list')
+ expect('Injection "listClick" not found').toHaveBeenWarned()
+ expect(warning).toHaveBeenTipped()
+ })
})
diff --git a/src/components/VList/VListTile.js b/src/components/VList/VListTile.js
index df2c947bd207..510846aa1bf3 100644
--- a/src/components/VList/VListTile.js
+++ b/src/components/VList/VListTile.js
@@ -1,11 +1,19 @@
+// Mixins
+import Colorable from '../../mixins/colorable'
import Routable from '../../mixins/routable'
import Toggleable from '../../mixins/toggleable'
+
+// Directives
import Ripple from '../../directives/ripple'
export default {
name: 'v-list-tile',
- mixins: [Routable, Toggleable],
+ mixins: [
+ Colorable,
+ Routable,
+ Toggleable
+ ],
directives: {
Ripple
@@ -28,6 +36,13 @@ export default {
},
computed: {
+ listClasses () {
+ return this.disabled
+ ? 'text--disabled'
+ : this.color
+ ? this.addTextColorClassChecks()
+ : this.defaultColor
+ },
classes () {
return {
'list__tile': true,
@@ -56,6 +71,7 @@ export default {
data.attrs = Object.assign({}, data.attrs, this.$attrs)
return h('li', {
+ 'class': this.listClasses,
attrs: {
disabled: this.disabled
},
diff --git a/src/components/VList/VListTile.spec.js b/src/components/VList/VListTile.spec.js
index 8023b1a14c82..6009464a787b 100644
--- a/src/components/VList/VListTile.spec.js
+++ b/src/components/VList/VListTile.spec.js
@@ -1,5 +1,5 @@
-import { test } from '~util/testing'
-import { VListTile } from '~components/VList'
+import { test } from '@util/testing'
+import { VListTile } from '@components/VList'
import { compileToFunctions } from 'vue-template-compiler'
import Vue from 'vue/dist/vue.common'
diff --git a/src/components/VList/VListTileAction.js b/src/components/VList/VListTileAction.js
index a777dd5ce2e7..7097f7b9fb90 100644
--- a/src/components/VList/VListTileAction.js
+++ b/src/components/VList/VListTileAction.js
@@ -4,7 +4,7 @@ export default {
name: 'v-list-tile-action',
render (h, { data, children }) {
- data.staticClass = data.staticClass ? `list__tile__action ${data.staticClass || ''}` : 'list__tile__action'
+ data.staticClass = data.staticClass ? `list__tile__action ${data.staticClass}` : 'list__tile__action'
if ((children || []).length > 1) data.staticClass += ' list__tile__action--stack'
return h('div', data, children)
diff --git a/src/components/VList/VListTileAction.spec.js b/src/components/VList/VListTileAction.spec.js
index a301e78c8994..c0ec08d3c205 100644
--- a/src/components/VList/VListTileAction.spec.js
+++ b/src/components/VList/VListTileAction.spec.js
@@ -1,5 +1,6 @@
-import { VListTileAction } from '~components/VList'
-import { test } from '~util/testing'
+import Vue from 'vue'
+import { VListTileAction } from '@components/VList'
+import { test } from '@util/testing'
test('VListTileAction.js', ({ mount, functionalContext }) => {
it('should render component and match snapshot', () => {
@@ -7,4 +8,24 @@ test('VListTileAction.js', ({ mount, functionalContext }) => {
expect(wrapper.html()).toMatchSnapshot()
})
+
+ it('should render component with static class and match snapshot', () => {
+ const wrapper = mount(VListTileAction, functionalContext({
+ staticClass: 'static-class'
+ }))
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+
+ it('should render component with many children and match snapshot', () => {
+ const content1 = mount(Vue.component('content1', {
+ render: h => h('div')
+ })).vNode
+ const content2 = mount(Vue.component('content2', {
+ render: h => h('span')
+ })).vNode
+ const wrapper = mount(VListTileAction, functionalContext({}, [content1, content2]))
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
})
diff --git a/src/components/VList/VListTileAvatar.js b/src/components/VList/VListTileAvatar.js
new file mode 100644
index 000000000000..4fd6e424d4a3
--- /dev/null
+++ b/src/components/VList/VListTileAvatar.js
@@ -0,0 +1,29 @@
+// Components
+import VAvatar from '../VAvatar'
+
+export default {
+ functional: true,
+
+ name: 'v-list-tile-avatar',
+
+ props: {
+ color: String,
+ size: {
+ type: [Number, String],
+ default: 40
+ }
+ },
+
+ render (h, { data, children, props }) {
+ data.staticClass = (`list__tile__avatar ${data.staticClass || ''}`).trim()
+
+ const avatar = h(VAvatar, {
+ props: {
+ color: props.color,
+ size: props.size
+ }
+ }, [children])
+
+ return h('div', data, [avatar])
+ }
+}
diff --git a/src/components/VList/VListTileAvatar.spec.js b/src/components/VList/VListTileAvatar.spec.js
new file mode 100755
index 000000000000..79546d25f5a1
--- /dev/null
+++ b/src/components/VList/VListTileAvatar.spec.js
@@ -0,0 +1,10 @@
+import { VListTileAvatar } from '@components/VList'
+import { test } from '@util/testing'
+
+test('VListTileAvatar.js', ({ mount, functionalContext }) => {
+ it('should render component and match snapshot', () => {
+ const wrapper = mount(VListTileAvatar, functionalContext())
+
+ expect(wrapper.html()).toMatchSnapshot()
+ })
+})
diff --git a/src/components/VList/__snapshots__/VList.spec.js.snap b/src/components/VList/__snapshots__/VList.spec.js.snap
index b107857c0f62..f62363e575d1 100644
--- a/src/components/VList/__snapshots__/VList.spec.js.snap
+++ b/src/components/VList/__snapshots__/VList.spec.js.snap
@@ -2,45 +2,35 @@
exports[`VList.js should render a dense component and match snapshot 1`] = `
-
+
`;
exports[`VList.js should render a subheader component and match snapshot 1`] = `
-