Try to dispatch an accept event on blur

(re: #5752 - but does not close it)
This commit is contained in:
Bryan Housel
2019-01-25 14:45:47 -05:00
parent 8f5ab67a9c
commit 55715a4827
2 changed files with 41 additions and 31 deletions

View File

@@ -1,3 +1,5 @@
import _clone from 'lodash-es/clone';
import { import {
dispatch as d3_dispatch dispatch as d3_dispatch
} from 'd3-dispatch'; } from 'd3-dispatch';
@@ -11,11 +13,10 @@ import { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';
// This code assumes that the combobox values will not have duplicate entries. // This code assumes that the combobox values will not have duplicate entries.
// It is keyed on the `value` of the entry. // It is keyed on the `value` of the entry. Data should be an array of objects like:
// Data should be an array of objects like:
// [{ // [{
// title: 'hover text', // value: 'display text', // required
// value: 'display text' // title: 'hover text' // optional
// }, ...] // }, ...]
var _comboHideTimerID; var _comboHideTimerID;
@@ -25,7 +26,8 @@ export function uiCombobox(context, klass) {
var container = context.container(); var container = context.container();
var _suggestions = []; var _suggestions = [];
var _values = []; var _data = [];
var _fetched = {};
var _selected = null; var _selected = null;
var _canAutocomplete = true; var _canAutocomplete = true;
var _caseSensitive = false; var _caseSensitive = false;
@@ -34,7 +36,7 @@ export function uiCombobox(context, klass) {
var _tDown = 0; var _tDown = 0;
var _fetcher = function(val, cb) { var _fetcher = function(val, cb) {
cb(_values.filter(function(d) { cb(_data.filter(function(d) {
return d.value return d.value
.toString() .toString()
.toLowerCase() .toLowerCase()
@@ -114,6 +116,9 @@ export function uiCombobox(context, klass) {
function blur() { function blur() {
// Try to dispatch accept here, but no guarantee - see note in `accept`
accept(null, true); // null = datum, true = onBlur
_comboHideTimerID = window.setTimeout(hide, 75); _comboHideTimerID = window.setTimeout(hide, 75);
} }
@@ -213,18 +218,6 @@ export function uiCombobox(context, klass) {
} }
// return the datum for the currently chosen value
function datum(val) {
for (var i = 0; i < _suggestions.length; i++) {
var suggestion = _suggestions[i];
if (suggestion.value === val) {
return suggestion;
}
}
return null;
}
// Called whenever the input value is changed (e.g. on typing) // Called whenever the input value is changed (e.g. on typing)
function change() { function change() {
fetch(value(), function() { fetch(value(), function() {
@@ -237,7 +230,7 @@ export function uiCombobox(context, klass) {
} }
if (!_selected) { if (!_selected) {
_selected = datum(val); _selected = val;
} }
} }
@@ -261,7 +254,7 @@ export function uiCombobox(context, klass) {
// try to determine previously selected index.. // try to determine previously selected index..
var index = -1; var index = -1;
for (var i = 0; i < _suggestions.length; i++) { for (var i = 0; i < _suggestions.length; i++) {
if (_selected && _suggestions[i].value === _selected.value) { if (_selected && _suggestions[i].value === _selected) {
index = i; index = i;
break; break;
} }
@@ -269,8 +262,8 @@ export function uiCombobox(context, klass) {
// pick new _selected // pick new _selected
index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0); index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);
_selected = _suggestions[index]; _selected = _suggestions[index].value;
input.property('value', _selected.value); input.property('value', _selected);
} }
render(); render();
@@ -316,8 +309,12 @@ export function uiCombobox(context, klass) {
_cancelFetch = false; _cancelFetch = false;
_fetcher.call(input, v, function(results) { _fetcher.call(input, v, function(results) {
// already chose a value, don't overwrite or autocomplete it
if (_cancelFetch) return; if (_cancelFetch) return;
_suggestions = results; _suggestions = results;
results.forEach(function(d) { _fetched[d.value] = d; });
if (cb) { if (cb) {
cb(); cb();
} }
@@ -354,7 +351,7 @@ export function uiCombobox(context, klass) {
var bestVal = _suggestions[bestIndex].value; var bestVal = _suggestions[bestIndex].value;
input.property('value', bestVal); input.property('value', bestVal);
input.node().setSelectionRange(val.length, bestVal.length); input.node().setSelectionRange(val.length, bestVal.length);
return _suggestions[bestIndex]; return bestVal;
} }
} }
@@ -379,10 +376,10 @@ export function uiCombobox(context, klass) {
options.enter() options.enter()
.append('a') .append('a')
.attr('class', 'combobox-option') .attr('class', 'combobox-option')
.text(function(d) { return d.value; })
.merge(options)
.attr('title', function(d) { return d.title; }) .attr('title', function(d) { return d.title; })
.classed('selected', function(d) { return d === _selected; }) .text(function(d) { return d.display || d.value; })
.merge(options)
.classed('selected', function(d) { return d.value === _selected; })
.on('click.combobox', accept) .on('click.combobox', accept)
.order(); .order();
@@ -398,7 +395,7 @@ export function uiCombobox(context, klass) {
// Dispatches an 'accept' event // Dispatches an 'accept' event
// Then hides the combobox. // Then hides the combobox.
function accept(d) { function accept(d, onBlur) {
_cancelFetch = true; _cancelFetch = true;
var thiz = input.node(); var thiz = input.node();
@@ -411,7 +408,18 @@ export function uiCombobox(context, klass) {
var val = utilGetSetValue(input); var val = utilGetSetValue(input);
thiz.setSelectionRange(val.length, val.length); thiz.setSelectionRange(val.length, val.length);
d = datum(val); d = _fetched[val];
// Try to dispatch `accept` onBlur, but only if we matched field to fetched datum.
// Surprisingly, this might happen:
// - user accepts a value in raw tag editor by pressing 'tab'
// - we dispatch `accept`
// - value change kicks off an event cascade *which replaces the combo*
// - 'tab' takes the user to the next field, blurring the current one
// - we get here, but the combo is new and has no datum yet
// - so just return because there's no reason to fire another `accept` with no datum
if (onBlur && !d) return;
dispatch.call('accept', thiz, d, val); dispatch.call('accept', thiz, d, val);
hide(); hide();
} }
@@ -451,8 +459,8 @@ export function uiCombobox(context, klass) {
}; };
combobox.data = function(val) { combobox.data = function(val) {
if (!arguments.length) return _values; if (!arguments.length) return _data;
_values = val; _data = val;
return combobox; return combobox;
}; };

View File

@@ -105,7 +105,7 @@ describe('uiCombobox', function() {
expect(body.selectAll('.combobox-option').nodes()[2].text).to.equal('Baz'); expect(body.selectAll('.combobox-option').nodes()[2].text).to.equal('Baz');
}); });
it('shows all entries when clicking on the caret', function() { it('shows all entries when activating the combo', function() {
input.property('value', 'foobar').call(combobox.data(data)); input.property('value', 'foobar').call(combobox.data(data));
focusTypeahead(input); focusTypeahead(input);
simulateKeypress('↓'); simulateKeypress('↓');
@@ -238,6 +238,7 @@ describe('uiCombobox', function() {
it('emits accepted event with selected datum on ⇥', function(done) { it('emits accepted event with selected datum on ⇥', function(done) {
combobox.on('accept', function(d) { combobox.on('accept', function(d) {
expect(d).to.eql({title: 'bar', value: 'bar'}); expect(d).to.eql({title: 'bar', value: 'bar'});
combobox.on('accept', null);
done(); done();
}); });
input.call(combobox.data(data)); input.call(combobox.data(data));
@@ -249,6 +250,7 @@ describe('uiCombobox', function() {
it('emits accepted event with selected datum on ↩', function(done) { it('emits accepted event with selected datum on ↩', function(done) {
combobox.on('accept', function(d) { combobox.on('accept', function(d) {
expect(d).to.eql({title: 'bar', value: 'bar'}); expect(d).to.eql({title: 'bar', value: 'bar'});
combobox.on('accept', null);
done(); done();
}); });
input.call(combobox.data(data)); input.call(combobox.data(data));