commit 902e2284c04f461ed3d3b3b9edd20607ea6de7e1
parent 412e58ecc8c02ddd312b5af3544d6cc77ee9fc63
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Wed, 27 Oct 2021 09:03:53 -0700
Improve arrow key behavior in TimeInput component
Diffstat:
6 files changed, 202 insertions(+), 209 deletions(-)
diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue
@@ -45,11 +45,11 @@ export default {
},
/**
- * Whether to wrap around at the minimum and maximum values
+ * Whether to allow the user to increment/decrement the value using the arrow keys
*/
- wrap: {
+ arrowKeys: {
type: Boolean,
- default: false,
+ default: true,
},
/**
@@ -183,19 +183,13 @@ export default {
* @param {Object} e The keydown event args
*/
onkeydown(e) {
- if (e.key === 'ArrowUp') {
- if (this.decValue === this.max && this.wrap && this.min !== null) {
- this.decValue = this.min;
- } else {
- this.decValue += this.step;
- }
+ if (!this.arrowKeys) {
+ this.$emit('keydown', e);
+ } else if (e.key === 'ArrowUp') {
+ this.decValue += this.step;
e.preventDefault();
} else if (e.key === 'ArrowDown') {
- if (this.decValue === this.min && this.wrap && this.max !== null) {
- this.decValue = this.max;
- } else {
- this.decValue -= this.step;
- }
+ this.decValue -= this.step;
e.preventDefault();
}
},
diff --git a/src/components/IntInput.vue b/src/components/IntInput.vue
@@ -45,11 +45,11 @@ export default {
},
/**
- * Whether to wrap around at the minimum and maximum values
+ * Whether to allow the user to increment/decrement the value using the arrow keys
*/
- wrap: {
+ arrowKeys: {
type: Boolean,
- default: false,
+ default: true,
},
/**
@@ -172,19 +172,13 @@ export default {
* @param {Object} e The keydown event args
*/
onkeydown(e) {
- if (e.key === 'ArrowUp') {
- if (this.intValue === this.max && this.wrap && this.min !== null) {
- this.intValue = this.min;
- } else {
- this.intValue += this.step;
- }
+ if (!this.arrowKeys) {
+ this.$emit('keydown', e);
+ } else if (e.key === 'ArrowUp') {
+ this.intValue += this.step;
e.preventDefault();
} else if (e.key === 'ArrowDown') {
- if (this.intValue === this.min && this.wrap && this.max !== null) {
- this.intValue = this.max;
- } else {
- this.intValue -= this.step;
- }
+ this.intValue -= this.step;
e.preventDefault();
}
},
diff --git a/src/components/TimeInput.vue b/src/components/TimeInput.vue
@@ -1,13 +1,16 @@
<template>
<div class="time-input">
<int-input class="hours" aria-label="hours"
- :min="0" :max="99" :padding="1" v-model="hours"/>
+ :min="0" :max="99" :padding="1" v-model="hours"
+ :arrow-keys="false" @keydown="onkeydown($event, 3600)"/>
<span>:</span>
<int-input class="minutes" aria-label="minutes"
- :min="0" :max="59" wrap :padding="2" v-model="minutes"/>
+ :min="0" :max="59" :padding="2" v-model="minutes"
+ :arrow-keys="false" @keydown="onkeydown($event, 60)"/>
<span>:</span>
<decimal-input class="seconds" aria-label="seconds"
- :min="0" :max="59.99" wrap :padding="2" :digits="2" v-model="seconds"/>
+ :min="0" :max="59.99" :padding="2" :digits="2" v-model="seconds"
+ :arrow-keys="false" @keydown="onkeydown($event, 1)"/>
</div>
</template>
@@ -39,28 +42,47 @@ export default {
data() {
return {
/**
- * The number of hours in the component value
+ * The internal value
*/
- hours: Math.floor(this.value / 3600),
-
- /**
- * The number of minutes in the component value
- */
- minutes: Math.floor((this.value % 3600) / 60),
-
- /**
- * The number of seconds in the component value
- */
- seconds: this.value % 60,
+ internalValue: this.value,
};
},
computed: {
/**
- * The value of the component
+ * The value of the hours field
+ */
+ hours: {
+ get() {
+ return Math.floor(this.value / 3600);
+ },
+ set(newValue) {
+ this.internalValue = (newValue * 3600) + (this.minutes * 60) + this.seconds;
+ },
+ },
+
+ /**
+ * The value of the minutes field
+ */
+ minutes: {
+ get() {
+ return Math.floor((this.value % 3600) / 60);
+ },
+ set(newValue) {
+ this.internalValue = (this.hours * 3600) + (newValue * 60) + this.seconds;
+ },
+ },
+
+ /**
+ * The value of the seconds field
*/
- intValue() {
- return (this.hours * 3600) + (this.minutes * 60) + this.seconds;
+ seconds: {
+ get() {
+ return this.value % 60;
+ },
+ set(newValue) {
+ this.internalValue = (this.hours * 3600) + (this.minutes * 60) + newValue;
+ },
},
},
@@ -70,10 +92,8 @@ export default {
* @param {Number} newValue The new prop value
*/
value(newValue) {
- if (newValue !== this.intValue) {
- this.hours = Math.floor(newValue / 3600);
- this.minutes = Math.floor((newValue % 3600) / 60);
- this.seconds = newValue % 60;
+ if (newValue !== this.internalValue) {
+ this.internalValue = newValue;
}
},
@@ -81,10 +101,34 @@ export default {
* Emit the input event when the component value changes
* @param {Number} newValue The new component value
*/
- intValue(newValue) {
+ internalValue(newValue) {
this.$emit('input', newValue);
},
},
+
+ methods: {
+ /**
+ * Process up and down arrow presses
+ * @param {Object} e The keydown event args
+ */
+ onkeydown(e, step = 1) {
+ if (e.key === 'ArrowUp') {
+ if (this.internalValue + step > 359999.99) {
+ this.internalValue = 359999.99;
+ } else {
+ this.internalValue += step;
+ }
+ e.preventDefault();
+ } else if (e.key === 'ArrowDown') {
+ if (this.internalValue - step < 0) {
+ this.internalValue = 0;
+ } else {
+ this.internalValue -= step;
+ }
+ e.preventDefault();
+ }
+ },
+ },
};
</script>
diff --git a/tests/unit/components/DecimalInput.spec.js b/tests/unit/components/DecimalInput.spec.js
@@ -252,91 +252,26 @@ describe('components/DecimalInput.vue', () => {
expect(wrapper.emitted().input).to.deep.equal([[10.0]]);
});
- it('should not wrap to the maximum if it is null', async () => {
+ it('should format value according to padding and digits props', async () => {
// Initialize component
const wrapper = mount(DecimalInput, {
- propsData: {
- min: -1.0, max: null, value: -1.0, step: 0.2, wrap: true,
- },
+ propsData: { padding: 2, digits: 3 },
});
- // Try to decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is still -1.0 and no events were emitted
- expect(wrapper.find('input').element.value).to.equal('-1.0');
- expect(wrapper.emitted().input).to.equal(undefined);
+ // Assert value is correctly formatted
+ expect(wrapper.find('input').element.value).to.equal('00.000');
});
- it('should not wrap to the minimum if it is null', async () => {
+ it('should emit keydown event if arrow-keys is false', async () => {
// Initialize component
const wrapper = mount(DecimalInput, {
- propsData: {
- min: null, max: 1.0, value: 1.0, step: 0.2, wrap: true,
- },
+ propsData: { arrowKeys: false },
});
// Try to increment value
await wrapper.trigger('keydown', { key: 'ArrowUp' });
- // Assert value is still 1.0 and no events were emitted
- expect(wrapper.find('input').element.value).to.equal('1.0');
- expect(wrapper.emitted().input).to.equal(undefined);
- });
-
- it('should correctly wrap from the minimum to maximum', async () => {
- // Initialize component
- const wrapper = mount(DecimalInput, {
- propsData: {
- min: -1.0, max: 1.0, value: -0.9, step: 0.2, wrap: true,
- },
- });
-
- // Decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is -1.0 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('-1.0');
- expect(wrapper.emitted().input).to.deep.equal([[-1.0]]);
-
- // Decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is 1.0 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('1.0');
- expect(wrapper.emitted().input).to.deep.equal([[-1.0], [1.0]]);
- });
-
- it('should correctly wrap from the maximum to minimum', async () => {
- // Initialize component
- const wrapper = mount(DecimalInput, {
- propsData: {
- min: -1.0, max: 1.0, value: 0.9, step: 0.2, wrap: true,
- },
- });
-
- // Increment value
- await wrapper.trigger('keydown', { key: 'ArrowUp' });
-
- // Assert value is 1.0 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('1.0');
- expect(wrapper.emitted().input).to.deep.equal([[1.0]]);
-
- // Increment value
- await wrapper.trigger('keydown', { key: 'ArrowUp' });
-
- // Assert value is -1.0 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('-1.0');
- expect(wrapper.emitted().input).to.deep.equal([[1.0], [-1.0]]);
- });
-
- it('should format value according to padding and digits props', async () => {
- // Initialize component
- const wrapper = mount(DecimalInput, {
- propsData: { padding: 2, digits: 3 },
- });
-
- // Assert value is correctly formatted
- expect(wrapper.find('input').element.value).to.equal('00.000');
+ // Assert keydown event emitted
+ expect(wrapper.emitted().keydown.length).to.equal(1);
});
});
diff --git a/tests/unit/components/IntInput.spec.js b/tests/unit/components/IntInput.spec.js
@@ -230,91 +230,26 @@ describe('components/IntInput.vue', () => {
expect(wrapper.emitted().input).to.deep.equal([[10]]);
});
- it('should not wrap to the maximum if it is null', async () => {
+ it('should format value according to padding prop', async () => {
// Initialize component
const wrapper = mount(IntInput, {
- propsData: {
- min: -10, max: null, value: -10, step: 2, wrap: true,
- },
+ propsData: { padding: 2 },
});
- // Try to decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is still -10 and no events were emitted
- expect(wrapper.find('input').element.value).to.equal('-10');
- expect(wrapper.emitted().input).to.equal(undefined);
+ // Assert value is correctly formatted
+ expect(wrapper.find('input').element.value).to.equal('00');
});
- it('should not wrap to the minimum if it is null', async () => {
+ it('should emit keydown event if arrow-keys is false', async () => {
// Initialize component
const wrapper = mount(IntInput, {
- propsData: {
- min: null, max: 10, value: 10, step: 2, wrap: true,
- },
+ propsData: { arrowKeys: false },
});
// Try to increment value
await wrapper.trigger('keydown', { key: 'ArrowUp' });
- // Assert value is still 10 and no events were emitted
- expect(wrapper.find('input').element.value).to.equal('10');
- expect(wrapper.emitted().input).to.equal(undefined);
- });
-
- it('should correctly wrap from the minimum to maximum', async () => {
- // Initialize component
- const wrapper = mount(IntInput, {
- propsData: {
- min: -10, max: 10, value: -9, step: 2, wrap: true,
- },
- });
-
- // Decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is -10 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('-10');
- expect(wrapper.emitted().input).to.deep.equal([[-10]]);
-
- // Decrement value
- await wrapper.trigger('keydown', { key: 'ArrowDown' });
-
- // Assert value is 10 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('10');
- expect(wrapper.emitted().input).to.deep.equal([[-10], [10]]);
- });
-
- it('should correctly wrap from the maximum to minimum', async () => {
- // Initialize component
- const wrapper = mount(IntInput, {
- propsData: {
- min: -10, max: 10, value: 9, step: 2, wrap: true,
- },
- });
-
- // Increment value
- await wrapper.trigger('keydown', { key: 'ArrowUp' });
-
- // Assert value is 10 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('10');
- expect(wrapper.emitted().input).to.deep.equal([[10]]);
-
- // Increment value
- await wrapper.trigger('keydown', { key: 'ArrowUp' });
-
- // Assert value is -10 and input event was emitted
- expect(wrapper.find('input').element.value).to.equal('-10');
- expect(wrapper.emitted().input).to.deep.equal([[10], [-10]]);
- });
-
- it('should format value according to padding prop', async () => {
- // Initialize component
- const wrapper = mount(IntInput, {
- propsData: { padding: 2 },
- });
-
- // Assert value is correctly formatted
- expect(wrapper.find('input').element.value).to.equal('00');
+ // Assert keydown event emitted
+ expect(wrapper.emitted().keydown.length).to.equal(1);
});
});
diff --git a/tests/unit/components/TimeInput.spec.js b/tests/unit/components/TimeInput.spec.js
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle */
import { expect } from 'chai';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import TimeInput from '@/components/TimeInput.vue';
describe('components/TimeInput.vue', () => {
@@ -10,9 +10,9 @@ describe('components/TimeInput.vue', () => {
const wrapper = shallowMount(TimeInput);
// Assert value is 0:00:00.00
- expect(wrapper.vm._data.hours).to.equal(0);
- expect(wrapper.vm._data.minutes).to.equal(0);
- expect(wrapper.vm._data.seconds).to.equal(0.00);
+ expect(wrapper.vm.hours).to.equal(0);
+ expect(wrapper.vm.minutes).to.equal(0);
+ expect(wrapper.vm.seconds).to.equal(0.00);
});
it('should read value prop', () => {
@@ -22,9 +22,9 @@ describe('components/TimeInput.vue', () => {
});
// Assert value is 1:01:01.50
- expect(wrapper.vm._data.hours).to.equal(1);
- expect(wrapper.vm._data.minutes).to.equal(1);
- expect(wrapper.vm._data.seconds).to.equal(1.50);
+ expect(wrapper.vm.hours).to.equal(1);
+ expect(wrapper.vm.minutes).to.equal(1);
+ expect(wrapper.vm.seconds).to.equal(1.50);
});
it('should update when value prop changes', async () => {
@@ -35,9 +35,9 @@ describe('components/TimeInput.vue', () => {
await wrapper.setProps({ value: 60 });
// Assert value is 0:01:00.00
- expect(wrapper.vm._data.hours).to.equal(0);
- expect(wrapper.vm._data.minutes).to.equal(1);
- expect(wrapper.vm._data.seconds).to.equal(0.00);
+ expect(wrapper.vm.hours).to.equal(0);
+ expect(wrapper.vm.minutes).to.equal(1);
+ expect(wrapper.vm.seconds).to.equal(0.00);
});
it('should emit input event when value changes', async () => {
@@ -45,15 +45,106 @@ describe('components/TimeInput.vue', () => {
const wrapper = shallowMount(TimeInput);
// Change value to 1:00:00.00
- await wrapper.setData({ hours: 1 });
+ await wrapper.setData({ internalValue: 3600 });
// Assert input event was emitted
expect(wrapper.emitted().input).to.deep.equal([[3600.00]]);
// Change value to 1:00:01.50
- await wrapper.setData({ seconds: 1.5 });
+ await wrapper.setData({ internalValue: 3601.5 });
// Assert another input event was emitted
expect(wrapper.emitted().input).to.deep.equal([[3600.00], [3601.50]]);
});
+
+ it('up arrow should increment value', async () => {
+ // Initialize component
+ const wrapper = mount(TimeInput, {
+ propsData: { value: 59 },
+ });
+
+ // Press up arrow in hours field
+ await wrapper.find('input.hours').trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 01:00:59.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(3659);
+ expect(wrapper.emitted().input).to.deep.equal([[3659]]);
+
+ // Press up arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 01:01:00.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(3660);
+ expect(wrapper.emitted().input).to.deep.equal([[3659], [3660]]);
+ });
+
+ it('up arrow should not increment value past the maximum', async () => {
+ // Initialize component
+ const wrapper = mount(TimeInput, {
+ propsData: { value: 359998 },
+ });
+
+ // Press up arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 99:59:59.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(359999);
+ expect(wrapper.emitted().input).to.deep.equal([[359999]]);
+
+ // Press up arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 99:59:59.99 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(359999.99);
+ expect(wrapper.emitted().input).to.deep.equal([[359999], [359999.99]]);
+
+ // Press up arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is still 99:59:59.99 and input event was not emitted
+ expect(wrapper.vm.internalValue).to.equal(359999.99);
+ expect(wrapper.emitted().input).to.deep.equal([[359999], [359999.99]]);
+ });
+
+ it('down arrow should decrement value', async () => {
+ // Initialize component
+ const wrapper = mount(TimeInput, {
+ propsData: { value: 3660 },
+ });
+
+ // Press down arrow in hours field
+ await wrapper.find('input.hours').trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is 00:01:00.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(60);
+ expect(wrapper.emitted().input).to.deep.equal([[60]]);
+
+ // Press down arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is 00:00:59.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(59);
+ expect(wrapper.emitted().input).to.deep.equal([[60], [59]]);
+ });
+
+ it('down arrow should not decrement value past the minimum', async () => {
+ // Initialize component
+ const wrapper = mount(TimeInput, {
+ propsData: { value: 1 },
+ });
+
+ // Press down arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is 00:00:00.00 and input event was emitted
+ expect(wrapper.vm.internalValue).to.equal(0);
+ expect(wrapper.emitted().input).to.deep.equal([[0]]);
+
+ // Press down arrow in seconds field
+ await wrapper.find('input.seconds').trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is still 00:00:00.00 and input event was not emitted
+ expect(wrapper.vm.internalValue).to.equal(0);
+ expect(wrapper.emitted().input).to.deep.equal([[0]]);
+ });
});