Skip to content

Commit

Permalink
Make autocomplete offer enum values separately. (#4349)
Browse files Browse the repository at this point in the history
* Make autocomplete offer enum values separately.

* Clarified helper function.
  • Loading branch information
jrobbins authored Sep 12, 2024
1 parent 419c936 commit dc68cdf
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 7 deletions.
24 changes: 20 additions & 4 deletions client-src/elements/chromedash-feature-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,27 @@ import {createRef, Ref, ref} from 'lit/directives/ref.js';
import {SHARED_STYLES} from '../css/shared-css.js';
import {openSearchHelpDialog} from './chromedash-search-help-dialog.js';
import {QUERIABLE_FIELDS} from './queriable-fields.js';
import {ChromedashTypeahead} from './chromedash-typeahead.js';
import {ChromedashTypeahead, Candidate} from './chromedash-typeahead.js';

const VOCABULARY = QUERIABLE_FIELDS.map(qf => {
return {name: qf.name + '=', doc: qf.doc};
});
function convertQueriableFieldToVocabularyItems(qf): Candidate[] {
if (qf.choices === undefined) {
return [{group: qf.name, name: qf.name + '=', doc: qf.doc}];
}
const result: Candidate[] = [];
for (const ch in qf.choices) {
const label: string = qf.choices[ch][1];
result.push({
group: qf.name,
name: qf.name + '="' + label + '"',
doc: qf.doc,
});
}
return result;
}

const VOCABULARY: Candidate[] = QUERIABLE_FIELDS.map(
convertQueriableFieldToVocabularyItems
).flat();

@customElement('chromedash-feature-filter')
class ChromedashFeatureFilter extends LitElement {
Expand Down
50 changes: 47 additions & 3 deletions client-src/elements/chromedash-typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {SHARED_STYLES} from '../css/shared-css.js';
3. Private class ChromedashTypeaheadItem renders a single item in the
typeahead menu. We do not use SlMenuItem because it steals keyboard focus.
*/
interface Candidate {
export interface Candidate {
group: string;
name: string;
doc: string;
}
Expand Down Expand Up @@ -130,6 +131,40 @@ export class ChromedashTypeahead extends LitElement {
);
}

// Return true if the user is still entering the keyword and is not
// ready to enter an enum value yet.
shouldGroup(s: string | null): boolean {
if (s === null) {
return true;
}
const COMPARE_OPS = ['=', ':', '<', '>'];
return !COMPARE_OPS.some(op => s.includes(op));
}

groupCandidates(candidates: Candidate[]): Candidate[] {
const groupsSeen = new Set();
const groupsSeenTwice = new Set();
for (const c of candidates) {
if (groupsSeen.has(c.group)) {
groupsSeenTwice.add(c.group);
} else {
groupsSeen.add(c.group);
}
}

const groupsSeenTwiceProcessed = new Set();
const result: Candidate[] = [];
for (const c of candidates) {
if (!groupsSeenTwice.has(c.group)) {
result.push(c);
} else if (!groupsSeenTwiceProcessed.has(c.group)) {
result.push({group: c.group, name: c.group + '=', doc: c.doc});
groupsSeenTwiceProcessed.add(c.group);
}
}
return result;
}

async handleCandidateSelected(e) {
const candidateValue = e.detail.item.value;
const inputEl = this.slInputRef.value?.renderRoot.querySelector('input');
Expand All @@ -150,8 +185,14 @@ export class ChromedashTypeahead extends LitElement {
this.chunkEnd = this.chunkStart;
inputEl.selectionStart = this.chunkStart;
inputEl.selectionEnd = this.chunkEnd;
// TODO(jrobbins): Don't set termWasCompleted if we offer a value.
this.termWasCompleted = true;

// A term was completed iff there is no other term that the user could
// further complete by typing or selecting.
const possibleExtensions = this.vocabulary.filter(c =>
c.name.startsWith(candidateValue)
);
this.termWasCompleted = possibleExtensions.length <= 1;

this.calcCandidates();
// The user may have clicked a menu item, causing the sl-input to lose
// keyboard focus. So, focus on the sl-input again.
Expand Down Expand Up @@ -198,6 +239,9 @@ export class ChromedashTypeahead extends LitElement {
this.candidates = this.vocabulary.filter(c =>
this.shouldShowCandidate(c, this.prefix)
);
if (this.shouldGroup(this.prefix)) {
this.candidates = this.groupCandidates(this.candidates);
}
const slDropdown = this.slDropdownRef.value;
if (!slDropdown) return;
if (
Expand Down
37 changes: 37 additions & 0 deletions client-src/elements/chromedash-typeahead_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,43 @@ describe('chromedash-typeahead', () => {
assert.isFalse(component.shouldShowCandidate(candidate2, 'th'));
assert.isFalse(component.shouldShowCandidate(candidate2, 'th.dot'));
});

it('detects when user is still entering keyword', async () => {
const component = new ChromedashTypeahead();
assert.isTrue(component.shouldGroup(null));
assert.isTrue(component.shouldGroup(''));
assert.isTrue(component.shouldGroup('some-words'));
assert.isFalse(component.shouldGroup('field='));
assert.isFalse(component.shouldGroup('field>'));
assert.isFalse(component.shouldGroup('field>='));
assert.isFalse(component.shouldGroup('field<='));
assert.isFalse(component.shouldGroup('field!='));
assert.isFalse(component.shouldGroup('field:'));
assert.isFalse(component.shouldGroup('field>3'));
assert.isFalse(component.shouldGroup('field="enum value"'));
});

it('copes with empty candidate lists while grouping', async () => {
const component = new ChromedashTypeahead();
assert.deepEqual([], component.groupCandidates([]));
});

it('groups candidates that have the same group value', async () => {
const component = new ChromedashTypeahead();
const candidates = [
{group: 'a', name: 'a=1', doc: 'doc'},
{group: 'b', name: 'b=1', doc: 'doc'},
{group: 'c', name: 'c=1', doc: 'doc'},
{group: 'b', name: 'b=2', doc: 'doc'},
{group: 'b', name: 'b=3', doc: 'doc'},
];
const actual = component.groupCandidates(candidates);
assert.deepEqual(actual, [
{group: 'a', name: 'a=1', doc: 'doc'},
{group: 'b', name: 'b=', doc: 'doc'},
{group: 'c', name: 'c=1', doc: 'doc'},
]);
});
});

describe('chromedash-typeahead-dropdown', () => {
Expand Down

0 comments on commit dc68cdf

Please sign in to comment.