running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

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:
Msrc/components/DecimalInput.vue | 22++++++++--------------
Msrc/components/IntInput.vue | 22++++++++--------------
Msrc/components/TimeInput.vue | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mtests/unit/components/DecimalInput.spec.js | 81++++++++-----------------------------------------------------------------------
Mtests/unit/components/IntInput.spec.js | 81++++++++-----------------------------------------------------------------------
Mtests/unit/components/TimeInput.spec.js | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
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]]); + }); });