Visitar URL original
USWDS - Combo box: Enhance combo box results by mahoneycm · Pull Request #6122 · uswds/uswds · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 91 additions & 18 deletions packages/usa-combo-box/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,28 +382,101 @@ const displayList = (el) => {
const filter = comboBoxEl.dataset.filter || DEFAULT_FILTER;
const regex = generateDynamicRegExp(filter, inputValue, comboBoxEl.dataset);

const options = [];
for (let i = 0, len = selectEl.options.length; i < len; i += 1) {
const optionEl = selectEl.options[i];
const optionId = `${listOptionBaseId}${options.length}`;

if (
optionEl.value &&
(disableFiltering ||
isPristine ||
!inputValue ||
regex.test(optionEl.text))
) {
if (selectEl.value && optionEl.value === selectEl.value) {
selectedItemId = optionId;
}
let options = [];
const optionsStartsWith = [];
const optionsContains = [];
const optionList = [...selectEl.options];

/**
* Builds and sorts options array.
*
* Option param is passed through regex test before passing into this function.
* When filtering is enabled, the array will be sorted by options that start with the query, followed by
* options that contain the query.
Comment thread
mahoneycm marked this conversation as resolved.
Resolved
Comment thread
mahoneycm marked this conversation as resolved.
Resolved
* When filtering is disabled, all options will be included in the array unsorted.
*
* These array items will populate the list that is displayed to the user after a search query is entered.
* Array attributes are also used to set option IDs and aria-setsize attributes.
*
* @param {HTMLOptionElement} option - Option element from select array
*/
const buildOptionsArray = (option) => {
if (disableFiltering || isPristine) {
options.push(option);
Comment on lines +404 to +405

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: adding isPristine here prevents selected values from appearing at the top of the combo box. This allows the selected item to appear in the correct order, which matches current combo box behavior

return;
}

const matchStartsWith = option.text.toLowerCase().startsWith(inputValue);

if (matchStartsWith) {
optionsStartsWith.push(option);
} else {
optionsContains.push(option);
}

options = [...optionsStartsWith, ...optionsContains];
};

/**
* Compares option text to query using generated regex filter.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - True when option text matches user input query.
*/
const optionMatchesQuery = (option) => regex.test(option.text);

/**
* Logic check to determine if options array needs to be updated.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - True when option has value && if filtering is disabled, combo box has an active selection,
* there is no inputValue, or if option matches user query
*/
const arrayNeedsUpdate = (option) =>
option.value &&
(disableFiltering ||
isPristine ||
!inputValue ||
optionMatchesQuery(option));

/**
* Checks if firstFoundId should be assigned, which is then used to set itemToFocus.
*
* @param {HTMLOptionElement} option
* @return {boolean} - Returns true if filtering is disabled, no firstFoundId is assigned, and the option matches the query.
*/
const isFirstMatch = (option) =>
Comment thread
mahoneycm marked this conversation as resolved.
Resolved
Comment thread
amyleadem marked this conversation as resolved.
Resolved
disableFiltering && !firstFoundId && optionMatchesQuery(option);

/**
* Checks if isCurrentSelection should be assigned, which is then used to set itemToFocus.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - Returns true if option.value matches selectEl.value.
*/
const isCurrentSelection = (option) =>
selectEl.value && option.value === selectEl.value;

/**
* Update the array of options that should be displayed on the page.
* Assign an ID to each displayed option.
* Identify and assign the option that should receive focus.
*/
optionList.forEach((option) => {
Comment thread
mahoneycm marked this conversation as resolved.
Resolved
Comment thread
amyleadem marked this conversation as resolved.
Resolved
if (arrayNeedsUpdate(option)) {
buildOptionsArray(option);

if (disableFiltering && !firstFoundId && regex.test(optionEl.text)) {
const optionId = `${listOptionBaseId}${options.indexOf(option)}`;

if (isFirstMatch(option)) {
firstFoundId = optionId;
}
options.push(optionEl);

if (isCurrentSelection(option)) {
selectedItemId = optionId;
}
}
}
});

const numOptions = options.length;
const optionHtml = options.map((option, index) => {
Expand Down
22 changes: 22 additions & 0 deletions packages/usa-combo-box/src/test/combo-box.spec.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thanks for updating and adding unit tests!

Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,28 @@ tests.forEach(({ name, selector: containerSelector }) => {
);
});

it("should sort matches by options that start with the query, then options that contain the query", () => {
Comment thread
mejiaj marked this conversation as resolved.
Resolved
input.value = "tan";

EVENTS.input(input);

assert.strictEqual(
list.children.length,
3,
"should display a filtered list based on input",
);
assert.strictEqual(
list.firstChild.dataset.value,
"tangelo",
"should sort filtered items and put items that start with the query first",
);
assert.strictEqual(
list.lastChild.dataset.value,
"rambutan",
"should sort filtered items and put items that contain the query last",
);
});

it("should reset input values when an incomplete item is remaining on blur", () => {
select.value = "apricot";
input.value = "a";
Expand Down