Add keyboard navigation to date|time picker

This commit is contained in:
Sergio Crisostomo 2018-05-18 13:06:43 +02:00
parent 2bf3e04753
commit 75cb299868
16 changed files with 467 additions and 67 deletions

View file

@ -1,37 +1,47 @@
<template>
<div :class="[prefixCls + '-confirm']">
<span :class="timeClasses" v-if="showTime" @click="handleToggleTime">
<template v-if="isTime">{{ t('i.datepicker.selectDate') }}</template>
<template v-else>{{ t('i.datepicker.selectTime') }}</template>
</span>
<i-button size="small" type="text" @click.native="handleClear">{{ t('i.datepicker.clear') }}</i-button>
<i-button size="small" type="primary" @click.native="handleSuccess">{{ t('i.datepicker.ok') }}</i-button>
<div :class="[prefixCls + '-confirm']" @keydown.tab.capture="handleTab">
<i-button :class="timeClasses" size="small" type="text" :disabled="timeDisabled" v-if="showTime" @click="handleToggleTime">
{{labels.time}}
</i-button>
<i-button size="small" type="ghost" @click.native="handleClear" @keydown.enter.native="handleClear">
{{labels.clear}}
</i-button>
<i-button size="small" type="primary" @click.native="handleSuccess" @keydown.enter.native="handleSuccess">
{{labels.ok}}
</i-button>
</div>
</template>
<script>
import iButton from '../../button/button.vue';
import Locale from '../../../mixins/locale';
import Emitter from '../../../mixins/emitter';
const prefixCls = 'ivu-picker';
export default {
mixins: [ Locale ],
components: { iButton },
mixins: [Locale, Emitter],
components: {iButton},
props: {
showTime: false,
isTime: false,
timeDisabled: false
},
data () {
data() {
return {
prefixCls: prefixCls
};
},
computed: {
timeClasses () {
return {
[`${prefixCls}-confirm-time-disabled`]: this.timeDisabled
};
return `${prefixCls}-confirm-time`;
},
labels(){
const labels = ['time', 'clear', 'ok'];
const values = [(this.isTime ? 'selectDate' : 'selectTime'), 'clear', 'ok'];
return labels.reduce((obj, key, i) => {
obj[key] = this.t('i.datepicker.' + values[i]);
return obj;
}, {});
}
},
methods: {
@ -44,6 +54,17 @@
handleToggleTime () {
if (this.timeDisabled) return;
this.$emit('on-pick-toggle-time');
this.dispatch('CalendarPicker', 'focus-input');
},
handleTab(e) {
const tabbables = [...this.$el.children];
const expectedFocus = tabbables[e.shiftKey ? 'shift' : 'pop']();
if (document.activeElement === expectedFocus) {
e.preventDefault();
e.stopPropagation();
this.dispatch('CalendarPicker', 'focus-input');
}
}
}
};

View file

@ -9,7 +9,7 @@
:class="getCellCls(cell)"
v-for="(cell, i) in readCells"
:key="String(cell.date) + i"
@click="handleClick(cell)"
@click="handleClick(cell, $event)"
@mouseenter="handleMouseMove(cell)"
>
<em>{{ cell.desc }}</em>
@ -99,7 +99,9 @@
[`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth',
[`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth',
[`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel',
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end,
[`${prefixCls}-focused`]: clearHours(cell.date) === clearHours(this.focusedDate)
}
];
},

View file

@ -26,7 +26,10 @@ export default {
selecting: false
})
},
focusedDate: {
type: Date,
required: true,
}
},
computed: {
dates(){

View file

@ -38,14 +38,16 @@
const tableYear = this.tableDate.getFullYear();
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1)));
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1));
for (let i = 0; i < 12; i++) {
const cell = deepCopy(cell_tmpl);
cell.date = new Date(tableYear, i, 1);
cell.text = this.tCell(i + 1);
const time = clearHours(cell.date);
const day = clearHours(cell.date);
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month';
cell.selected = selectedDays.includes(time);
cell.selected = selectedDays.includes(day);
cell.focused = day === focusedDate;
cells.push(cell);
}
@ -59,6 +61,7 @@
{
[`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-disabled`]: cell.disabled,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
}
];

View file

@ -22,8 +22,10 @@
import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist';
const prefixCls = 'ivu-time-picker-cells';
const timeParts = ['hours', 'minutes', 'seconds'];
export default {
name: 'TimeSpinner',
mixins: [Options],
props: {
hours: {
@ -51,7 +53,9 @@
return {
spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one),
prefixCls: prefixCls,
compiled: false
compiled: false,
focusedColumn: -1, // which column inside the picker
focusedTime: [0, 0, 0] // the values array into [hh, mm, ss]
};
},
computed: {
@ -66,6 +70,7 @@
hoursList () {
let hours = [];
const step = this.spinerSteps[0];
const focusedHour = this.focusedColumn === 0 && this.focusedTime[0];
const hour_tmpl = {
text: 0,
selected: false,
@ -76,6 +81,7 @@
for (let i = 0; i < 24; i += step) {
const hour = deepCopy(hour_tmpl);
hour.text = i;
hour.focused = i === focusedHour;
if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) {
hour.disabled = true;
@ -90,6 +96,7 @@
minutesList () {
let minutes = [];
const step = this.spinerSteps[1];
const focusedMinute = this.focusedColumn === 1 && this.focusedTime[1];
const minute_tmpl = {
text: 0,
selected: false,
@ -100,6 +107,7 @@
for (let i = 0; i < 60; i += step) {
const minute = deepCopy(minute_tmpl);
minute.text = i;
minute.focused = i === focusedMinute;
if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) {
minute.disabled = true;
@ -113,6 +121,7 @@
secondsList () {
let seconds = [];
const step = this.spinerSteps[2];
const focusedMinute = this.focusedColumn === 2 && this.focusedTime[2];
const second_tmpl = {
text: 0,
selected: false,
@ -123,6 +132,7 @@
for (let i = 0; i < 60; i += step) {
const second = deepCopy(second_tmpl);
second.text = i;
second.focused = i === focusedMinute;
if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) {
second.disabled = true;
@ -141,15 +151,32 @@
`${prefixCls}-cell`,
{
[`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-disabled`]: cell.disabled
}
];
},
chooseValue(values){
const changes = timeParts.reduce((obj, part, i) => {
const value = values[i];
if (this[part] === value) return obj;
return {
...obj,
[part]: value
};
}, {});
if (Object.keys(changes).length > 0) {
this.emitChange(changes);
}
},
handleClick (type, cell) {
if (cell.disabled) return;
const data = {};
data[type] = cell.text;
this.$emit('on-change', data);
const data = {[type]: cell.text};
this.emitChange(data);
},
emitChange(changes){
this.$emit('on-change', changes);
this.$emit('on-pick-click');
},
scroll (type, index) {
@ -168,15 +195,19 @@
return index;
},
updateScroll () {
const times = ['hours', 'minutes', 'seconds'];
this.$nextTick(() => {
times.forEach(type => {
timeParts.forEach(type => {
this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]);
});
});
},
formatTime (text) {
return text < 10 ? '0' + text : text;
},
updateFocusedTime(col, time) {
this.focusedColumn = col;
this.focusedTime = time.slice();
}
},
watch: {
@ -191,6 +222,13 @@
seconds (val) {
if (!this.compiled) return;
this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val));
},
focusedTime(updated, old){
timeParts.forEach((part, i) => {
if (updated[i] === old[i] || typeof updated[i] === 'undefined') return;
const valueIndex = this[`${part}List`].findIndex(obj => obj.text === updated[i]);
this.scroll(part, valueIndex);
});
}
},
mounted () {

View file

@ -39,13 +39,15 @@
};
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1)));
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), 0, 1));
for (let i = 0; i < 10; i++) {
const cell = deepCopy(cell_tmpl);
cell.date = new Date(this.startYear + i, 0, 1);
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year';
const time = clearHours(cell.date);
cell.selected = selectedDays.includes(time);
const day = clearHours(cell.date);
cell.selected = selectedDays.includes(day);
cell.focused = day === focusedDate;
cells.push(cell);
}
@ -59,6 +61,7 @@
{
[`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-disabled`]: cell.disabled,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
}
];