From 46c3729e57d3e66010d2081a094007003d04082a Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 10 Oct 2023 17:03:56 -0400 Subject: [PATCH] `ToggleSelect` component (#362) * Add ToggleSelect component * Remove unused change * Update tests and test selectors * Update test, increase dropdown height * Remove comment --- web/app/components/document/sidebar.hbs | 2 +- .../index.hbs => product-select.hbs} | 28 ++------ .../index.ts => product-select.ts} | 8 +-- .../components/inputs/product-select/item.hbs | 32 +++++---- .../components/inputs/product-select/item.ts | 2 +- web/app/components/new/doc-form.hbs | 12 ++-- web/app/components/x/dropdown-list/_shared.ts | 2 +- web/app/components/x/dropdown-list/index.hbs | 16 +++++ web/app/components/x/dropdown-list/index.ts | 6 ++ web/app/components/x/dropdown-list/item.hbs | 1 + web/app/components/x/dropdown-list/item.ts | 3 +- .../x/dropdown-list/toggle-select.hbs | 19 ++++++ .../x/dropdown-list/toggle-select.ts | 18 +++++ web/app/styles/app.scss | 1 - .../inputs/product-select/index.scss | 19 ------ .../components/x/dropdown/toggle-select.scss | 6 +- .../acceptance/authenticated/new/doc-test.ts | 2 +- .../index-test.ts => product-select-test.ts} | 66 ++++++------------- .../inputs/product-select/item-test.ts | 32 +++++---- .../components/x/dropdown-list/index-test.ts | 51 ++++++++++++++ 20 files changed, 192 insertions(+), 134 deletions(-) rename web/app/components/inputs/{product-select/index.hbs => product-select.hbs} (73%) rename web/app/components/inputs/{product-select/index.ts => product-select.ts} (90%) create mode 100644 web/app/components/x/dropdown-list/toggle-select.hbs create mode 100644 web/app/components/x/dropdown-list/toggle-select.ts delete mode 100644 web/app/styles/components/inputs/product-select/index.scss rename web/tests/integration/components/inputs/{product-select/index-test.ts => product-select-test.ts} (63%) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 89dde2cb7..dfde65b32 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -147,7 +147,7 @@
{{#if (and this.isDraft this.isOwner)}} -
+
+
{{#if this.products}} {{#if @formatIsBadge}} <:anchor as |dd|> - - - - - {{or @selected "Select a product/area"}} - - - {{#if this.selectedProductAbbreviation}} - - {{this.selectedProductAbbreviation}} - - {{/if}} - - - + + + <:item as |dd|> diff --git a/web/app/components/inputs/product-select/index.ts b/web/app/components/inputs/product-select.ts similarity index 90% rename from web/app/components/inputs/product-select/index.ts rename to web/app/components/inputs/product-select.ts index b3a28c44d..af251e8f7 100644 --- a/web/app/components/inputs/product-select/index.ts +++ b/web/app/components/inputs/product-select.ts @@ -15,7 +15,7 @@ interface InputsProductSelectSignature { Element: HTMLDivElement; Args: { selected?: string; - onChange: (value: string, attributes?: ProductArea) => void; + onChange: (value: string, attributes: ProductArea) => void; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; @@ -42,16 +42,16 @@ export default class InputsProductSelectComponent extends Component - - {{@product}} - +
- {{#if @abbreviation}} - - {{@abbreviation}} - - {{/if}} + {{#if @product}} + + {{@product}} + + {{else}} + + {{/if}} + + {{#if @abbreviation}} + + {{@abbreviation}} + + {{/if}} +
{{#if @isSelected}} diff --git a/web/app/components/inputs/product-select/item.ts b/web/app/components/inputs/product-select/item.ts index 8a44e10b6..ccb127b39 100644 --- a/web/app/components/inputs/product-select/item.ts +++ b/web/app/components/inputs/product-select/item.ts @@ -3,7 +3,7 @@ import Component from "@glimmer/component"; interface InputsProductSelectItemComponentSignature { Element: HTMLDivElement; Args: { - product: string; + product?: string; isSelected?: boolean; abbreviation?: string; }; diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index 63c75020f..47ca20b38 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -72,11 +72,13 @@ Where your doc should be categorized.
- +
+ +
diff --git a/web/app/components/x/dropdown-list/_shared.ts b/web/app/components/x/dropdown-list/_shared.ts index 743157c09..835766311 100644 --- a/web/app/components/x/dropdown-list/_shared.ts +++ b/web/app/components/x/dropdown-list/_shared.ts @@ -21,7 +21,7 @@ export interface XDropdownListSharedArgs { } /** - * Used by ToggleAction and ToggleButton + * Used by ToggleAction, ToggleSelect and ToggleButton */ export interface XDropdownListToggleComponentArgs { registerAnchor: (e: HTMLElement) => void; diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index b5e4e93fa..0ea04dfcc 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -53,6 +53,22 @@ f.contentID ) ) + ToggleSelect=(component + "x/dropdown-list/toggle-select" + contentIsShown=f.contentIsShown + registerAnchor=f.registerAnchor + onTriggerKeydown=(fn + this.onTriggerKeydown f.contentIsShown f.showContent + ) + toggleContent=f.toggleContent + disabled=@disabled + ariaControls=(concat + "x-dropdown-list-" + (if this.inputIsShown "container" "items") + "-" + f.contentID + ) + ) ariaControls=(concat "x-dropdown-list-" (if this.inputIsShown "container" "items") diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index e54dd5be1..8c57ca65f 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -18,6 +18,7 @@ import { XDropdownListItemAPI } from "./item"; import { restartableTask, timeout } from "ember-concurrency"; import maybeScrollIntoView from "hermes/utils/maybe-scroll-into-view"; import { MatchAnchorWidthOptions } from "hermes/components/floating-u-i/content"; +import XDropdownListToggleSelectComponent from "./toggle-select"; export type XDropdownListToggleComponentBoundArgs = | "contentIsShown" @@ -37,7 +38,12 @@ export interface XDropdownListAnchorAPI typeof XDropdownListToggleButtonComponent, XDropdownListToggleComponentBoundArgs | "color" | "text" >; + ToggleSelect: WithBoundArgs< + typeof XDropdownListToggleSelectComponent, + XDropdownListToggleComponentBoundArgs + >; focusedItemIndex: number; + selected: any; resetFocusedItemIndex: () => void; scheduleAssignMenuItemIDs: () => void; hideContent: () => void; diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index cd3d22bf9..27be5e0d6 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -59,6 +59,7 @@ contentID=@contentID value=@value attrs=@attributes + selected=@selected isSelected=@isSelected ) }} diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index e2287ae7e..68250fd69 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -43,7 +43,7 @@ export interface XDropdownListItemComponentArgs { onItemClick?: (value: any, attributes: any) => void; setFocusedItemIndex: ( focusDirection: FocusDirection | number, - maybeScrollIntoView?: boolean + maybeScrollIntoView?: boolean, ) => void; hideContent: () => void; } @@ -51,6 +51,7 @@ export interface XDropdownListItemComponentArgs { interface XDropdownListItemComponentSignature { Args: XDropdownListItemComponentArgs & { value: string; + selected?: any; }; Blocks: { default: [dd: XDropdownListItemAPI]; diff --git a/web/app/components/x/dropdown-list/toggle-select.hbs b/web/app/components/x/dropdown-list/toggle-select.hbs new file mode 100644 index 000000000..99871e784 --- /dev/null +++ b/web/app/components/x/dropdown-list/toggle-select.hbs @@ -0,0 +1,19 @@ + + {{yield}} + + diff --git a/web/app/components/x/dropdown-list/toggle-select.ts b/web/app/components/x/dropdown-list/toggle-select.ts new file mode 100644 index 000000000..cddc539a0 --- /dev/null +++ b/web/app/components/x/dropdown-list/toggle-select.ts @@ -0,0 +1,18 @@ +import Component from "@glimmer/component"; +import { XDropdownListToggleComponentArgs } from "./_shared"; + +interface XDropdownListToggleSelectComponentSignature { + Element: HTMLButtonElement; + Args: XDropdownListToggleComponentArgs; + Blocks: { + default: []; + }; +} + +export default class XDropdownListToggleSelectComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "x/dropdown-list/toggle-select": typeof XDropdownListToggleSelectComponent; + } +} diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 49bd5cccc..79aa2b4a0 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -26,7 +26,6 @@ @use "components/header/active-filter-list"; @use "components/header/active-filter-list-item"; @use "components/header/search"; -@use "components/inputs/product-select/index.scss"; @use "components/dashboard/docs-awaiting-review/doc"; @use "components/doc/tile-list"; @use "components/doc/thumbnail"; diff --git a/web/app/styles/components/inputs/product-select/index.scss b/web/app/styles/components/inputs/product-select/index.scss deleted file mode 100644 index 5b5ad4c36..000000000 --- a/web/app/styles/components/inputs/product-select/index.scss +++ /dev/null @@ -1,19 +0,0 @@ -.product-select-dropdown-list { - @apply max-h-[217px]; -} - -.product-select-default-toggle { - @apply min-w-[300px] relative flex items-center justify-start leading-none; -} - -.product-select-toggle-abbreviation { - @apply opacity-50 inline-flex ml-2 text-body-100 leading-none; -} - -.product-select-selected-value { - @apply flex ml-2.5 leading-none; -} - -.product-select-toggle-caret { - @apply absolute top-1/2 -translate-y-1/2 right-2 text-color-foreground-faint; -} diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss index 91dced7b4..f881b2282 100644 --- a/web/app/styles/components/x/dropdown/toggle-select.scss +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -1,5 +1,5 @@ .x-dropdown-list-toggle-select { - @apply text-left border; + @apply relative flex min-h-[36px] w-full items-center justify-start gap-0 rounded-button-md border border-color-border-strong px-2.5 leading-none; .flight-icon { @apply shrink-0; @@ -19,9 +19,5 @@ &.hds-button--color-secondary:not(:hover) { @apply bg-color-surface-primary; - - .caret { - @apply text-color-foreground-faint; - } } } diff --git a/web/tests/acceptance/authenticated/new/doc-test.ts b/web/tests/acceptance/authenticated/new/doc-test.ts index 7a5091085..923dd735d 100644 --- a/web/tests/acceptance/authenticated/new/doc-test.ts +++ b/web/tests/acceptance/authenticated/new/doc-test.ts @@ -12,7 +12,7 @@ import { DRAFT_CREATED_LOCAL_STORAGE_KEY } from "hermes/components/modals/draft- // Selectors const DOC_FORM = "[data-test-new-doc-form]"; const PRODUCT_SELECT = `${DOC_FORM} [data-test-product-select]`; -const PRODUCT_SELECT_TOGGLE = `${PRODUCT_SELECT} [data-test-x-dropdown-list-toggle-action]`; +const PRODUCT_SELECT_TOGGLE = `${PRODUCT_SELECT} [data-test-x-dropdown-list-toggle-select]`; const CREATE_BUTTON = `${DOC_FORM} [data-test-create-button]`; const TITLE_INPUT = `${DOC_FORM} [data-test-title-input]`; const SUMMARY_INPUT = `${DOC_FORM} [data-test-summary-input]`; diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select-test.ts similarity index 63% rename from web/tests/integration/components/inputs/product-select/index-test.ts rename to web/tests/integration/components/inputs/product-select-test.ts index 1c60d376c..75a212c02 100644 --- a/web/tests/integration/components/inputs/product-select/index-test.ts +++ b/web/tests/integration/components/inputs/product-select-test.ts @@ -7,8 +7,9 @@ import { MirageTestContext } from "ember-cli-mirage/test-support"; import { Placement } from "@floating-ui/dom"; import { Response } from "miragejs"; -const DEFAULT_DROPDOWN_SELECTOR = ".product-select-default-toggle"; -const LIST_ITEM_SELECTOR = "[data-test-product-select-item]"; +const TOGGLE = "[data-test-x-dropdown-list-toggle-select]"; +const DROPDOWN_PRODUCT = + "[data-test-x-dropdown-list-content] [data-test-product-select-item]"; interface InputsProductSelectContext extends MirageTestContext { selected?: any; @@ -30,7 +31,9 @@ module("Integration | Component | inputs/product-select", function (hooks) { }); this.set("selected", "Vault"); - this.set("onChange", () => {}); + this.set("onChange", (value: string) => { + this.set("selected", value); + }); }); test("it can render in two formats", async function (this: InputsProductSelectContext, assert) { @@ -47,61 +50,31 @@ module("Integration | Component | inputs/product-select", function (hooks) { `); assert.dom(badgeDropdownSelector).exists("badge dropdown is rendered"); - assert - .dom(DEFAULT_DROPDOWN_SELECTOR) - .doesNotExist("default dropdown is not rendered"); + assert.dom(TOGGLE).doesNotExist("default dropdown is not rendered"); this.set("formatIsBadge", false); assert .dom(badgeDropdownSelector) .doesNotExist("badge dropdown is not rendered"); - assert - .dom(DEFAULT_DROPDOWN_SELECTOR) - .exists("default dropdown is rendered"); - }); - - test("it can render the toggle with a product abbreviation", async function (this: InputsProductSelectContext, assert) { - this.set("selected", this.server.schema.products.first().name); - - await render(hbs` - - `); - - assert.dom(".product-select-toggle-abbreviation").hasText("TP0"); - }); - - test("it shows an empty state when nothing is selected (default toggle)", async function (this: InputsProductSelectContext, assert) { - this.set("selected", undefined); - - await render(hbs` - - `); - assert - .dom(".product-select-selected-value") - .hasText("Select a product/area"); + assert.dom(TOGGLE).exists("default dropdown is rendered"); }); - test("it displays the products in a dropdown list with abbreviations", async function (this: InputsProductSelectContext, assert) { + test("it displays the products with abbreviations", async function (this: InputsProductSelectContext, assert) { await render(hbs` `); - await click(DEFAULT_DROPDOWN_SELECTOR); + assert.dom(TOGGLE).hasText("Vault VLT"); - assert.dom(LIST_ITEM_SELECTOR).exists({ count: 4 }); + await click(TOGGLE); - let firstListItem = this.element.querySelector(LIST_ITEM_SELECTOR); - assert.dom(firstListItem).hasText("Test Product 0 TP0"); + assert.dom(DROPDOWN_PRODUCT).exists({ count: 4 }); + assert.dom(DROPDOWN_PRODUCT).hasText("Test Product 0 TP0"); }); test("it fetches the products if they aren't already loaded", async function (this: InputsProductSelectContext, assert) { @@ -109,16 +82,17 @@ module("Integration | Component | inputs/product-select", function (hooks) { await render(hbs` `); - await click(DEFAULT_DROPDOWN_SELECTOR); + await click(TOGGLE); // In Mirage, we return a default product when there are no products in the database. // This simulates the `fetchProducts` task being run. - assert.dom(LIST_ITEM_SELECTOR).exists({ count: 1 }); - assert.dom(LIST_ITEM_SELECTOR).hasText("Default Fetched Product NONE"); + assert.dom(DROPDOWN_PRODUCT).exists({ count: 1 }); + assert.dom(DROPDOWN_PRODUCT).hasText("Default Fetched Product NONE"); }); test("it performs the passed-in action on click", async function (this: InputsProductSelectContext, assert) { @@ -134,8 +108,8 @@ module("Integration | Component | inputs/product-select", function (hooks) { /> `); - await click(DEFAULT_DROPDOWN_SELECTOR); - await click(LIST_ITEM_SELECTOR); + await click(TOGGLE); + await click(DROPDOWN_PRODUCT); assert.equal(count, 1, "the action was called once"); }); diff --git a/web/tests/integration/components/inputs/product-select/item-test.ts b/web/tests/integration/components/inputs/product-select/item-test.ts index 3d329a297..661f5cc2e 100644 --- a/web/tests/integration/components/inputs/product-select/item-test.ts +++ b/web/tests/integration/components/inputs/product-select/item-test.ts @@ -5,6 +5,11 @@ import { render } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; +const VALUE = "[data-test-selected-value]"; +const ABBREVIATION = "[data-test-product-select-item-abbreviation]"; +const PRODUCT_ICON = "[data-test-product-icon]"; +const CHECK = "[data-test-check]"; + interface InputsProductSelectItemContext extends MirageTestContext { product: string; isSelected?: boolean; @@ -28,37 +33,36 @@ module( /> `); - // assert that the icon has the "data-test-icon="vault" attribute assert - .dom("[data-test-product-select-item-icon]") + .dom(PRODUCT_ICON) .hasAttribute( "data-test-icon", "vault", "the correct product icon is shown", ); - assert - .dom("[data-test-product-select-item-value]") - .hasText("Vault", "the product name is rendered"); - - assert - .dom("[data-test-product-select-item-abbreviation]") - .doesNotExist("no abbreviation specified"); - - assert - .dom("[data-test-product-select-item-selected]") - .doesNotExist("check icon only rendered when selected"); + assert.dom(VALUE).hasText("Vault", "the product name is rendered"); + assert.dom(ABBREVIATION).doesNotExist("no abbreviation specified"); + assert.dom(CHECK).doesNotExist("check icon only rendered when selected"); this.set("product", "Engineering"); this.set("isSelected", true); assert - .dom("[data-test-product-select-item-icon]") + .dom(PRODUCT_ICON) .hasAttribute( "data-test-icon", "folder", "the correct product icon is shown", ); }); + + test("it shows an empty state when no product is provided", async function (this: InputsProductSelectItemContext, assert) { + await render(hbs` + + `); + + assert.dom("[data-test-empty-state]").hasText("Select a product/area"); + }); }, ); 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 c12a0e752..b083faac9 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -51,6 +51,7 @@ interface XDropdownListComponentTestContext extends TestContext { buttonWasClicked?: boolean; isLoading?: boolean; placement?: Placement | null; + selected?: string; } module("Integration | Component | x/dropdown-list", function (hooks) { @@ -941,4 +942,54 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom("[data-test-nothing]").exists("the custom empty state is shown"); }); + + test("it renders a ToggleSelect to the anchor API", async function (assert) { + this.set("items", {}); + + await render(hbs` + + <:anchor as |dd|> + + --- + + + + `); + + const toggleSelect = "[data-test-toggle]"; + const content = `.${CONTAINER_CLASS}`; + + assert.dom(toggleSelect).hasClass("x-dropdown-list-toggle-select"); + assert.dom(`${toggleSelect} [data-test-caret]`).exists(); + + assert.dom(content).doesNotExist(); + + await click(toggleSelect); + + assert.dom(content).exists(); + + await click(toggleSelect); + + assert.dom(content).doesNotExist(); + }); + + test("it renders the selected value to the anchor API", async function (assert) { + this.set("items", {}); + this.set("selected", "Foo"); + + await render(hbs` + + <:anchor as |dd|> +
+ {{dd.selected}} +
+ +
+ `); + + assert.dom("[data-test-div]").hasText("Foo"); + }); });