From e18013234c0ce3cc1a48d3416e5d579d6e5e4abd Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 26 Sep 2023 17:55:37 -0400 Subject: [PATCH] Add `secondaryFilterAttribute` to DropdownList (#349) * Add secondaryFilter support to DropdownList * Improve `inputIsShown` arg --- .../components/inputs/badge-dropdown-list.hbs | 1 + .../components/inputs/badge-dropdown-list.ts | 2 +- .../inputs/product-select/index.hbs | 2 + .../components/inputs/product-select/item.ts | 2 +- web/app/components/x/dropdown-list/index.ts | 34 +++-- .../components/x/dropdown-list/index-test.ts | 126 ++++++++++++------ 6 files changed, 116 insertions(+), 51 deletions(-) diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index 32c12473a..e469d1012 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -5,6 +5,7 @@ @selected={{@selected}} @placement={{@placement}} @renderOut={{@renderOut}} + @secondaryFilterAttribute={{@secondaryFilterAttribute}} ...attributes > <:anchor as |dd|> diff --git a/web/app/components/inputs/badge-dropdown-list.ts b/web/app/components/inputs/badge-dropdown-list.ts index 2ac9697d8..19c907ce6 100644 --- a/web/app/components/inputs/badge-dropdown-list.ts +++ b/web/app/components/inputs/badge-dropdown-list.ts @@ -9,9 +9,9 @@ interface InputsBadgeDropdownListComponentSignature { isSaving?: boolean; placement?: Placement; renderOut?: boolean; - onItemClick: ((e: Event) => void) | ((e: string) => void); icon: string; + secondaryFilterAttribute?: string; }; Blocks: { default: []; diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 19542e9c2..615521983 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -10,6 +10,7 @@ @placement={{@placement}} @isSaving={{@isSaving}} @renderOut={{@renderOut}} + @secondaryFilterAttribute="abbreviation" @icon={{this.icon}} class="product-select-dropdown-list w-80" ...attributes @@ -32,6 +33,7 @@ @placement={{@placement}} @isSaving={{@isSaving}} @renderOut={{@renderOut}} + @secondaryFilterAttribute="abbreviation" class="product-select-dropdown-list w-[300px]" ...attributes > diff --git a/web/app/components/inputs/product-select/item.ts b/web/app/components/inputs/product-select/item.ts index 1fb70e17c..8a44e10b6 100644 --- a/web/app/components/inputs/product-select/item.ts +++ b/web/app/components/inputs/product-select/item.ts @@ -5,7 +5,7 @@ interface InputsProductSelectItemComponentSignature { Args: { product: string; isSelected?: boolean; - abbreviation?: boolean; + abbreviation?: string; }; } diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 069c417ba..e54dd5be1 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -56,6 +56,15 @@ interface XDropdownListComponentSignature { label?: string; matchAnchorWidth?: MatchAnchorWidthOptions; + /** + * An additional attribute by which to search. + * Used to include secondary information when filtering. + * For example, we specify "abbreviation" for the `ProductSelect` + * component so that users can search by product's abbreviation + * in addition to its name. + */ + secondaryFilterAttribute?: string; + /** * Whether an asynchronous list is loading. * Used to determine if a loading UI is shown. @@ -144,8 +153,8 @@ export default class XDropdownListComponent extends Component void, - event: KeyboardEvent + event: KeyboardEvent, ) { if (contentIsShown) { return; @@ -311,7 +320,7 @@ export default class XDropdownListComponent extends Component { assert( "scheduleAssignMenuItemIDs expects a _scrollContainer", - this._scrollContainer + this._scrollContainer, ); this.assignMenuItemIDs( this._scrollContainer.querySelectorAll( - `[role=${this.listItemRole}]` - ) + `[role=${this.listItemRole}]`, + ), ); }); } else { if (i === 3) { throw new Error( - "scheduleAssignMenuItemIDs expects a _scrollContainer" + "scheduleAssignMenuItemIDs expects a _scrollContainer", ); } else { await timeout(1); diff --git a/web/tests/integration/components/x/dropdown-list/index-test.ts b/web/tests/integration/components/x/dropdown-list/index-test.ts index 345c7931d..c12a0e752 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -77,7 +77,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.ok( ariaControlsValue?.startsWith("x-dropdown-list-items"), - "the correct aria-controls attribute is set" + "the correct aria-controls attribute is set", ); await click("[data-test-toggle]"); @@ -97,13 +97,13 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.ok( ariaControlsValue?.startsWith(CONTAINER_CLASS), - "the correct aria-controls attribute is set" + "the correct aria-controls attribute is set", ); assert.equal( document.activeElement, this.element.querySelector(FILTER_INPUT_SELECTOR), - "the input is autofocused" + "the input is autofocused", ); }); @@ -142,6 +142,54 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom(DEFAULT_NO_MATCHES_SELECTOR).hasText("No matches"); }); + test("filtering works as expected when a secondary filter is passed in", async function (assert) { + this.set("items", { + foo: { + alias: "abc", + }, + bar: { + alias: "def", + }, + baz: { + alias: "foo", // Use an alias that matches the value of an item + }, + }); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click(TOGGLE_BUTTON_SELECTOR); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 3 }); + + await fillIn(FILTER_INPUT_SELECTOR, "foo"); + + assert + .dom("[data-test-x-dropdown-list-item]") + .exists( + { count: 2 }, + "the list is filtered by both the primary value and secondary filter", + ); + + await fillIn(FILTER_INPUT_SELECTOR, "abc"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 1 }); + }); + test("dropdown trigger has keyboard support", async function (assert) { this.set("items", LONG_ITEM_LIST); await render(hbs` @@ -285,7 +333,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.deepEqual( listItemIDs, ["0", "1", "2", "3", "4", "5", "6", "7"], - "the IDs are assigned in order" + "the IDs are assigned in order", ); }); @@ -317,15 +365,15 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.false( findAll("[data-test-item-button]").some((item) => - item.getAttribute("aria-selected") + item.getAttribute("aria-selected"), ), - "no items are aria-selected" + "no items are aria-selected", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.dom("#" + FIRST_ITEM_ID).hasAttribute("aria-selected"); @@ -333,7 +381,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.dom("#" + FIRST_ITEM_ID).doesNotHaveAttribute("aria-selected"); @@ -352,7 +400,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.dom("#" + LAST_ITEM_ID).doesNotHaveAttribute("aria-selected"); @@ -367,7 +415,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert .dom("[data-test-button-clicked]") .exists( - "keying Enter triggers the click action of the aria-selected item" + "keying Enter triggers the click action of the aria-selected item", ); assert @@ -395,9 +443,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.false( findAll("[data-test-item-button]").some((item) => - item.getAttribute("aria-selected") + item.getAttribute("aria-selected"), ), - "no items are aria-selected" + "no items are aria-selected", ); await triggerEvent("#" + FIRST_ITEM_ID, "mouseenter"); @@ -455,49 +503,49 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.true( itemBottom > scrollviewBottom, - "item four is not fully visible" + "item four is not fully visible", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.equal( itemBottom, item.offsetTop + itemHeight, - "container isn't scrolled unless the target is out of view" + "container isn't scrolled unless the target is out of view", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.equal( itemBottom, item.offsetTop + itemHeight, - "container isn't scrolled unless the target is out of view" + "container isn't scrolled unless the target is out of view", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); assert.equal( itemBottom, item.offsetTop + itemHeight, - "container isn't scrolled unless the target is out of view" + "container isn't scrolled unless the target is out of view", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); measure(); @@ -505,13 +553,13 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal( container.scrollTop, itemTop + itemHeight - containerHeight, - "item four scrolled into view" + "item four scrolled into view", ); await triggerKeyEvent( "[data-test-x-dropdown-list]", "keydown", - "ArrowDown" + "ArrowDown", ); measure("#x-dropdown-list-item-4"); @@ -519,7 +567,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal( container.scrollTop, itemTop + itemHeight - containerHeight, - "item five scrolled into view" + "item five scrolled into view", ); measure("#" + SECOND_ITEM_ID); @@ -533,7 +581,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal( itemTop, item.offsetTop, - "container isn't scrolled unless the target is out of view" + "container isn't scrolled unless the target is out of view", ); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); @@ -541,7 +589,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal( itemTop, item.offsetTop, - "container isn't scrolled unless the target is out of view" + "container isn't scrolled unless the target is out of view", ); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); @@ -576,7 +624,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal( firstLink.getAttribute("href"), "/documents?products=Labs", - "route and query are set" + "route and query are set", ); }); @@ -614,31 +662,31 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom(".flight-icon-chevron-up").exists(); const ariaControlsValue = htmlElement(TOGGLE_BUTTON_SELECTOR).getAttribute( - "aria-controls" + "aria-controls", ); const dropdownListItemsID = htmlElement( - ".x-dropdown-list-items" + ".x-dropdown-list-items", ).getAttribute("id"); assert.equal( ariaControlsValue, dropdownListItemsID, - "the aria-controls value matches the dropdown list ID" + "the aria-controls value matches the dropdown list ID", ); let dataAnchorID = htmlElement(TOGGLE_BUTTON_SELECTOR).getAttribute( - "data-anchor-id" + "data-anchor-id", ); let contentAnchoredTo = htmlElement("." + CONTAINER_CLASS).getAttribute( - "data-anchored-to" + "data-anchored-to", ); assert.equal( dataAnchorID, contentAnchoredTo, - "the anchor is properly registered" + "the anchor is properly registered", ); }); @@ -675,31 +723,31 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom("." + CONTAINER_CLASS).exists(); const ariaControlsValue = htmlElement(TOGGLE_ACTION_SELECTOR).getAttribute( - "aria-controls" + "aria-controls", ); const dropdownListItemsID = htmlElement( - ".x-dropdown-list-items" + ".x-dropdown-list-items", ).getAttribute("id"); assert.equal( ariaControlsValue, dropdownListItemsID, - "the aria-controls value matches the dropdown list ID" + "the aria-controls value matches the dropdown list ID", ); let dataAnchorID = htmlElement(TOGGLE_ACTION_SELECTOR).getAttribute( - "data-anchor-id" + "data-anchor-id", ); let contentAnchoredTo = htmlElement("." + CONTAINER_CLASS).getAttribute( - "data-anchored-to" + "data-anchored-to", ); assert.equal( dataAnchorID, contentAnchoredTo, - "the anchor is properly registered" + "the anchor is properly registered", ); }); @@ -751,7 +799,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { .dom(".target-div") .hasText( "View all items", - "the rendered-out item was place into the target div" + "the rendered-out item was place into the target div", ); assert