From 605fd5712999869a49d5a26c2b6fa604bba2aab3 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 28 Mar 2023 10:10:53 -0400 Subject: [PATCH 001/128] Add product dropdown to draft template --- web/app/components/document/sidebar.hbs | 36 ++++++++++++++++++++++--- web/app/components/document/sidebar.js | 22 ++++++++++++--- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 2aaf5e545..2349c99a6 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -168,10 +168,38 @@ Product/Area - + {{#if this.isDraft}} + {{!-- TODO: Do we need to save the product abbreviation? --}} + + {{#if this.products}} + + + {{#each-in this.products as |name|}} + + {{/each-in}} + + {{/if}} + + {{else}} + + {{/if}}
diff --git a/web/app/components/document/sidebar.js b/web/app/components/document/sidebar.js index 9bdaf368c..ea368e37f 100644 --- a/web/app/components/document/sidebar.js +++ b/web/app/components/document/sidebar.js @@ -12,7 +12,6 @@ export default class DocumentSidebar extends Component { @service router; @service session; @service flashMessages; - @tracked isCollapsed = false; @tracked archiveModalIsActive = false; @tracked deleteModalIsActive = false; @@ -42,9 +41,10 @@ export default class DocumentSidebar extends Component { @tracked title = this.args.document.title || ""; @tracked summary = this.args.document.summary || ""; @tracked tags = this.args.document.tags || []; - @tracked contributors = this.args.document.contributors || []; @tracked approvers = this.args.document.approvers || []; + @tracked product = this.args.document.product || ""; + @tracked products = null; get customEditableFields() { let customEditableFields = this.args.document.customEditableFields || {}; @@ -202,13 +202,17 @@ export default class DocumentSidebar extends Component { } } - @action refreshRoute() { // We force refresh due to a bug with `refreshModel: true` // See: https://github.com/emberjs/ember.js/issues/19260 getOwner(this).lookup(`route:${this.router.currentRouteName}`).refresh(); } + @action updateProduct(event) { + this.product = event.target.value; + this.save.perform("product", this.product); + } + @task *save(field, val) { if (field && val) { @@ -230,6 +234,18 @@ export default class DocumentSidebar extends Component { } } + @task *fetchProducts() { + try { + let products = yield this.fetchSvc + .fetch("/api/v1/products") + .then((resp) => resp.json()); + this.products = products; + } catch (err) { + console.error(err); + throw err; + } + } + @task *patchDocument(fields) { const endpoint = this.isDraft ? "drafts" : "documents"; From fde652ea298b34b6938321356af45d4d9337293d Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 30 Mar 2023 15:41:33 -0400 Subject: [PATCH 002/128] Add tests --- web/app/components/document/sidebar.hbs | 7 +- web/mirage/config.ts | 56 +++++++++++- web/mirage/factories/document.ts | 16 +++- web/mirage/factories/product.ts | 6 ++ web/mirage/models/product.ts | 5 + .../components/document/sidebar-test.ts | 91 +++++++++++++++++++ 6 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 web/mirage/factories/product.ts create mode 100644 web/mirage/models/product.ts create mode 100644 web/tests/integration/components/document/sidebar-test.ts diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 2349c99a6..0903eec35 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -75,7 +75,7 @@ (is-empty @document.docType) }}{{@document.docType}}{{/unless}} • - {{@document.docNumber}} + {{@document.docNumber}} {{/if}} {{#if this.editingIsDisabled}}

Product/Area {{#if this.isDraft}} - {{!-- TODO: Do we need to save the product abbreviation? --}} + {{#if this.products}} diff --git a/web/mirage/config.ts b/web/mirage/config.ts index 181063b8a..647ae39af 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -2,6 +2,7 @@ import { Collection, Response, createServer } from "miragejs"; import config from "../config/environment"; +import { getTestProductAbbreviation } from "./factories/document"; export default function (mirageConfig) { let finalConfig = { @@ -172,10 +173,40 @@ export default function (mirageConfig) { /** * Used by /subscriptions to get all possible subscriptions. - * Also used by the NewDoc route to map the products to their abbreviations. + * Used by the NewDoc route to map the products to their abbreviations. + * Used by the sidebar to populate a draft's product/area dropdown. */ this.get("/products", () => { - return; + let objects = this.schema.products.all().models.map((product) => { + return { + [product.attrs.name]: { + abbreviation: product.attrs.abbreviation, + }, + }; + }); + + // The objects currently look like: + // [ + // 0: { "Labs": { abbreviation: "LAB" } }, + // 1: { "Vault": { abbreviation: "VLT"} } + // ] + + // We need to reformat them to match the API's response. + + let formattedObjects = {}; + + objects.forEach((object) => { + let key = Object.keys(object)[0]; + formattedObjects[key] = object[key]; + }); + + // The formattedObjects now look look like: + // { + // "Labs": { abbreviation: "LAB" }, + // "Vault": { abbreviation: "VLT" } + // } + + return new Response(200, {}, formattedObjects); }); // RecentlyViewedDocsService / fetchIndexID @@ -219,6 +250,27 @@ export default function (mirageConfig) { return new Response(200, {}, schema.recentlyViewedDocs.all().models); } ); + + /** + * Used by the sidebar to save document properties, e.g., productArea. + */ + this.patch("/drafts/:document_id", (schema, request) => { + let document = schema.document.findBy({ + objectID: request.params.document_id, + }); + + if (document) { + let attrs = JSON.parse(request.requestBody); + + if ("product" in attrs) { + attrs.docNumber = getTestProductAbbreviation(attrs.product); + } + + document.update(attrs); + + return new Response(200, {}, document.attrs); + } + }); }, }; diff --git a/web/mirage/factories/document.ts b/web/mirage/factories/document.ts index f8b271bcc..b4928c274 100644 --- a/web/mirage/factories/document.ts +++ b/web/mirage/factories/document.ts @@ -1,13 +1,23 @@ import { Factory } from "miragejs"; +export function getTestProductAbbreviation(product: string) { + switch (product) { + case "Test Product 0": + return "TST-000"; + case "Test Product 1": + return "TST-001"; + } +} + export default Factory.extend({ - id: (i: number) => `doc-${i}`, objectID: (i: number) => `doc-${i}`, + title: "My Document", status: "Draft", product: "Vault", docType: "RFC", modifiedAgo: 1000000000, modifiedTime: 1000000000, - docNumber: "RFC-0000", - title: "My Document", + docNumber() { + return getTestProductAbbreviation(this.product); + }, }); diff --git a/web/mirage/factories/product.ts b/web/mirage/factories/product.ts new file mode 100644 index 000000000..2d0ae8e2d --- /dev/null +++ b/web/mirage/factories/product.ts @@ -0,0 +1,6 @@ +import { Factory } from "miragejs"; + +export default Factory.extend({ + name: (i: number) => `Test Product ${i}`, + abbreviation: (i: number) => `TST-${i}`, +}); diff --git a/web/mirage/models/product.ts b/web/mirage/models/product.ts new file mode 100644 index 000000000..dea7fc045 --- /dev/null +++ b/web/mirage/models/product.ts @@ -0,0 +1,5 @@ +import { Model } from "miragejs"; + +export default Model.extend({ + // Required for Mirage, even though it's empty +}); diff --git a/web/tests/integration/components/document/sidebar-test.ts b/web/tests/integration/components/document/sidebar-test.ts new file mode 100644 index 000000000..5cd0391f7 --- /dev/null +++ b/web/tests/integration/components/document/sidebar-test.ts @@ -0,0 +1,91 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { findAll, render, select } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; + +module("Integration | Component | document/sidebar", function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test("you can change a draft's product area", async function (this: MirageTestContext, assert) { + this.server.createList("product", 3); + + const docID = "test-doc-0"; + const profile = this.server.create("me"); + const document = this.server.create("document", { + objectID: docID, + isDraft: true, + product: "Test Product 1", + }); + + this.set("profile", profile); + this.set("document", document); + this.set("noop", () => {}); + + await render(hbs` + + `); + + assert + .dom("[data-test-sidebar-product-select]") + .exists("drafts show a product select element") + .hasValue("Test Product 1", "The document product is selected"); + + assert + .dom("[data-test-sidebar-doc-number]") + .hasText("TST-001", "The document number is correct"); + + const options = findAll("[data-test-sidebar-product-select] option"); + + const expectedProducts = [ + "", // The first option is blank + "Test Product 0", + "Test Product 1", + "Test Product 2", + ]; + + options.forEach((option: Element, index: number) => { + assert.equal( + option.textContent, + expectedProducts[index], + "the product is correct" + ); + }); + + await select("[data-test-sidebar-product-select]", "Test Product 0"); + + /** + * Mirage properties aren't reactive like Ember's, so we + * need to manually update the document. + */ + + const refreshMirageDocument = () => { + this.set( + "document", + this.server.schema.document.findBy({ objectID: docID }) + ); + }; + + refreshMirageDocument(); + + assert + .dom("[data-test-sidebar-doc-number]") + .hasText("TST-000", "The document is patched with the correct docNumber"); + + this.server.schema.document + .findBy({ objectID: docID }) + .update("isDraft", false); + + refreshMirageDocument(); + + assert + .dom("[data-test-sidebar-product-select]") + .doesNotExist("The product select is not shown for published documents"); + }); +}); From 0c51886dc90363f85a7a40eae7e07491350102de Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 7 Apr 2023 16:24:07 -0400 Subject: [PATCH 003/128] Start of custom popover --- web/app/components/document/sidebar.hbs | 8 +- web/app/components/document/sidebar.js | 17 +-- web/app/components/x/hds/popover.hbs | 23 ++++ web/app/components/x/hds/popover.ts | 63 ++++++++++ .../x/hds/product-badge-dropdown.hbs | 61 ++++++++++ .../x/hds/product-badge-dropdown.ts | 109 ++++++++++++++++++ web/app/helpers/html-element.ts | 6 + web/app/styles/app.scss | 1 + web/app/styles/components/popover.scss | 15 +++ web/app/styles/hds-overrides.scss | 8 ++ .../integration/helpers/html-element-test.ts | 23 ++++ 11 files changed, 318 insertions(+), 16 deletions(-) create mode 100644 web/app/components/x/hds/popover.hbs create mode 100644 web/app/components/x/hds/popover.ts create mode 100644 web/app/components/x/hds/product-badge-dropdown.hbs create mode 100644 web/app/components/x/hds/product-badge-dropdown.ts create mode 100644 web/app/helpers/html-element.ts create mode 100644 web/app/styles/components/popover.scss create mode 100644 web/tests/integration/helpers/html-element-test.ts diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 75401c2df..1bcae855b 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -134,7 +134,12 @@ class="hds-typography-body-100 hds-foreground-faint" >Product/Area {{#if this.isDraft}} - + + {{else}} resp.json()); - this.products = products; - } catch (err) { - console.error(err); - throw err; - } - } - @task *patchDocument(fields) { const endpoint = this.isDraft ? "drafts" : "documents"; diff --git a/web/app/components/x/hds/popover.hbs b/web/app/components/x/hds/popover.hbs new file mode 100644 index 000000000..008ad81ad --- /dev/null +++ b/web/app/components/x/hds/popover.hbs @@ -0,0 +1,23 @@ +{{#if @renderOut}} + {{#in-element (html-element ".ember-application") insertBefore=null}} +
+ {{yield}} +
+ {{/in-element}} +{{else}} +
+ {{yield}} +
+{{/if}} diff --git a/web/app/components/x/hds/popover.ts b/web/app/components/x/hds/popover.ts new file mode 100644 index 000000000..939268c8d --- /dev/null +++ b/web/app/components/x/hds/popover.ts @@ -0,0 +1,63 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import { guidFor } from "@ember/object/internals"; +import { + Placement, + autoUpdate, + computePosition, + flip, + offset, + platform, + shift, +} from "@floating-ui/dom"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import htmlElement from "hermes/utils/html-element"; + +interface XHdsPopoverSignature { + Args: { + anchor: HTMLElement; + placement?: Placement; + renderOut?: boolean; + }; +} + +export default class XHdsPopover extends Component { + @tracked _popover: HTMLElement | null = null; + + get id() { + return guidFor(this); + } + + get popover() { + assert("_popover must exist", this._popover); + return this._popover; + } + + get arrow() { + return htmlElement(`#popover-${this.id} .arrow`); + } + + @tracked cleanup: (() => void) | null = null; + + @action didInsert(e: HTMLElement) { + this._popover = e; + + let updatePosition = async () => { + computePosition(this.args.anchor, this.popover, { + platform: platform, + placement: this.args.placement || "bottom", + middleware: [offset(5), flip(), shift()], + }).then(({ x, y, placement }) => { + this.popover.setAttribute("data-popover-placement", placement); + + Object.assign(this.popover.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + }; + + this.cleanup = autoUpdate(this.args.anchor, this.popover, updatePosition); + } +} diff --git a/web/app/components/x/hds/product-badge-dropdown.hbs b/web/app/components/x/hds/product-badge-dropdown.hbs new file mode 100644 index 000000000..f962b13f1 --- /dev/null +++ b/web/app/components/x/hds/product-badge-dropdown.hbs @@ -0,0 +1,61 @@ +{{#if @readOnly}} + +{{else}} + +
+ + +
+
+ {{#if this.popoverIsShown}} + +
+ {{! TODO: Make this dependent on the number of products }} + {{#if this.inputIsShown}} +
+ +
+ {{/if}} +
    + {{#each-in this.shownProducts as |product|}} +
  1. {{product}}
  2. + {{/each-in}} +
+
+
+ {{/if}} +{{/if}} diff --git a/web/app/components/x/hds/product-badge-dropdown.ts b/web/app/components/x/hds/product-badge-dropdown.ts new file mode 100644 index 000000000..a4133a82a --- /dev/null +++ b/web/app/components/x/hds/product-badge-dropdown.ts @@ -0,0 +1,109 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { task } from "ember-concurrency"; +import FetchService from "hermes/services/fetch"; + +interface XHdsBadgeDropdownSignature { + Args: { + currentProduct: string; + options: string[]; + onChange: (value: string) => void; + readonly: boolean; + }; +} + +type ProductAreas = { + [key: string]: { + abbreviation: string; + perDocDataType: unknown; + }; +}; + +export default class XHdsBadgeDropdown extends Component { + @service("fetch") declare fetchSvc: FetchService; + + @tracked _trigger: HTMLElement | null = null; + @tracked _products: ProductAreas | undefined = undefined; + @tracked _input: HTMLInputElement | null = null; + + @tracked popoverIsShown = false; + + @tracked filteredProducts: ProductAreas | null = null; + + get shownProducts() { + return this.filteredProducts || this.products; + } + + get trigger() { + assert("trigger must exist", this._trigger); + return this._trigger; + } + + get products() { + assert("products must exist", this._products); + return this._products; + } + + get input() { + assert("input must exist", this._input); + return this._input; + } + + @action togglePopover() { + if (this.popoverIsShown) { + this.hidePopover(); + } else { + this.popoverIsShown = true; + } + } + + @action hidePopover() { + this.popoverIsShown = false; + this.filteredProducts = null; + } + + get inputIsShown() { + // if the number of object keys in the products + // is greater than 1, then show the input + return Object.keys(this.products).length > 1; + } + + @action didInsertTrigger(e: HTMLElement) { + this._trigger = e; + void this.fetchProducts.perform(); + } + + @action registerAndFocusInput(e: HTMLInputElement) { + this._input = e; + this.input.focus(); + } + + @action onInput(e: InputEvent) { + // filter the products by the input value + let value = this.input.value; + if (value) { + this.filteredProducts = Object.fromEntries( + Object.entries(this.products).filter(([key]) => + key.toLowerCase().includes(value.toLowerCase()) + ) + ); + } else { + this.filteredProducts = null; + } + } + + protected fetchProducts = task(async () => { + try { + let products = await this.fetchSvc + .fetch("/api/v1/products") + .then((resp) => resp?.json()); + this._products = products; + } catch (err) { + console.error(err); + throw err; + } + }); +} diff --git a/web/app/helpers/html-element.ts b/web/app/helpers/html-element.ts new file mode 100644 index 000000000..d0c69b887 --- /dev/null +++ b/web/app/helpers/html-element.ts @@ -0,0 +1,6 @@ +import { helper } from "@ember/component/helper"; +import htmlElement from "hermes/utils/html-element"; + +export default helper(([selector]: [string]) => { + return htmlElement(selector); +}); diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index fe6a59d75..fc9c84442 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -1,6 +1,7 @@ @use "components/action"; @use "components/toolbar"; @use "components/tooltip"; +@use "components/popover"; @use "components/footer"; @use "components/nav"; @use "components/x-hds-tab"; diff --git a/web/app/styles/components/popover.scss b/web/app/styles/components/popover.scss new file mode 100644 index 000000000..1e38f841f --- /dev/null +++ b/web/app/styles/components/popover.scss @@ -0,0 +1,15 @@ +.hermes-popover { + @apply absolute bg-color-foreground-high-contrast rounded z-50 hds-dropdown-list; + + /* These positioning styles are overwritten by FloatingUI, + * but they ensure that the tooltip isn't added to the bottom of the page, + * where it could cause a reflow. This is especially important because + * the Google Docs iframe responds to layout changes and might + * otherwise jitter when a tooltip opened. + */ + @apply top-0 left-0; + + .text { + @apply relative; + } +} diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index 52c4710e3..19fb846e4 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -30,3 +30,11 @@ } } } + +.hds-badge-dropdown { + @apply pr-6; + + + .dropdown-caret { + @apply absolute right-1.5 top-1/2 -translate-y-1/2; + } +} diff --git a/web/tests/integration/helpers/html-element-test.ts b/web/tests/integration/helpers/html-element-test.ts new file mode 100644 index 000000000..a592bfd79 --- /dev/null +++ b/web/tests/integration/helpers/html-element-test.ts @@ -0,0 +1,23 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import MockDate from "mockdate"; + +module("Integration | Helper | html-element", function (hooks) { + setupRenderingTest(hooks); + + test("", async function (assert) { + await render(hbs` + {{#in-element (html-element ".container")}} +
+ Like magic +
+ {{/in-element}} + +
+ `); + + assert.dom(".container .content").hasText("Like magic"); + }); +}); From 849b965dc1fb329ba440e902d7a26499d36fe5c4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 14:24:24 -0400 Subject: [PATCH 004/128] Start generic popover component --- configs/config.hcl | 338 +++++++++--------- web/app/components/document/sidebar.hbs | 2 +- web/app/components/floating-u-i.hbs | 18 + web/app/components/floating-u-i.ts | 24 ++ .../hds/{popover.hbs => popover/content.hbs} | 0 .../x/hds/{popover.ts => popover/content.ts} | 0 web/app/components/x/hds/popover/index.hbs | 43 +++ .../index.ts} | 19 +- .../components/x/hds/popover/list-item.hbs | 15 + web/app/components/x/hds/popover/list-item.ts | 7 + web/app/components/x/hds/popover/list.hbs | 52 +++ web/app/components/x/hds/popover/list.ts | 29 ++ .../x/hds/product-badge-dropdown.hbs | 61 ---- web/app/router.js | 3 +- web/app/styles/components/popover.scss | 3 +- web/app/templates/playground.hbs | 13 + 16 files changed, 379 insertions(+), 248 deletions(-) create mode 100644 web/app/components/floating-u-i.hbs create mode 100644 web/app/components/floating-u-i.ts rename web/app/components/x/hds/{popover.hbs => popover/content.hbs} (100%) rename web/app/components/x/hds/{popover.ts => popover/content.ts} (100%) create mode 100644 web/app/components/x/hds/popover/index.hbs rename web/app/components/x/hds/{product-badge-dropdown.ts => popover/index.ts} (84%) create mode 100644 web/app/components/x/hds/popover/list-item.hbs create mode 100644 web/app/components/x/hds/popover/list-item.ts create mode 100644 web/app/components/x/hds/popover/list.hbs create mode 100644 web/app/components/x/hds/popover/list.ts delete mode 100644 web/app/components/x/hds/product-badge-dropdown.hbs create mode 100644 web/app/templates/playground.hbs diff --git a/configs/config.hcl b/configs/config.hcl index 5cae2fd3b..b831c0297 100644 --- a/configs/config.hcl +++ b/configs/config.hcl @@ -1,169 +1,169 @@ -// base_url is the base URL used for building links. This should be the public -// URL of the application. -base_url = "http://localhost:8000" - -// algolia configures Hermes to work with Algolia. -algolia { - application_id = "" - docs_index_name = "docs" - drafts_index_name = "drafts" - internal_index_name = "internal" - links_index_name = "links" - missing_fields_index_name = "missing_fields" - search_api_key = "" - write_api_key = "" -} - -// document_types configures document types. Currently this block should not be -// modified, but Hermes will support custom document types in the near future. -// *** DO NOT MODIFY document_types *** -document_types { - document_type "RFC" { - long_name = "Request for Comments" - description = "Create a Request for Comments document to present a proposal to colleagues for their review and feedback." - template = "1Oz_7FhaWxdFUDEzKCC5Cy58t57C4znmC_Qr80BORy1U" - - more_info_link { - text = "More info on the RFC template" - url = "https://works.hashicorp.com/articles/rfc-template" - } - - custom_field { - name = "Current Version" - type = "string" - } - custom_field { - name = "PRD" - type = "string" - } - custom_field { - name = "Stakeholders" - type = "people" - } - custom_field { - name = "Target Version" - type = "string" - } - } - - document_type "PRD" { - long_name = "Product Requirements" - description = "Create a Product Requirements Document to summarize a problem statement and outline a phased approach to addressing the problem." - template = "1oS4q6IPDr3aMSTTk9UDdOnEcFwVWW9kT8ePCNqcg1P4" - - more_info_link { - text = "More info on the PRD template" - url = "https://works.hashicorp.com/articles/prd-template" - } - - custom_field { - name = "RFC" - type = "string" - } - custom_field { - name = "Stakeholders" - type = "people" - } - } -} - -// email configures Hermes to send email notifications. -email { - // enabled enables sending email notifications. - enabled = true - - // from_address is the email address to send email notifications from. - from_address = "hermes@yourorganization.com" -} - -// google_workspace configures Hermes to work with Google Workspace. -google_workspace { - // create_doc_shortcuts enables creating a shortcut in the shortcuts_folder - // when a document is published. - create_doc_shortcuts = true - - // docs_folder contains all published documents in a flat structure. - docs_folder = "my-docs-folder-id" - - // drafts_folder contains all draft documents. - drafts_folder = "my-drafts-folder-id" - - // If create_doc_shortcuts is set to true, shortcuts_folder will contain an - // organized hierarchy of folders and shortcuts to published files that can be - // easily browsed directly in Google Drive: - // {shortcut_folder}/{doc_type}/{product}/{document} - shortcuts_folder = "my-shortcuts-folder-id" - - // auth is the configuration for interacting with Google Workspace using a - // service account. - // auth { - // client_email = "" - // private_key = "" - // subject = "" - // token_url = "https://oauth2.googleapis.com/token" - // } - - // oauth2 is the configuration used to authenticate users via Google. - oauth2 { - client_id = "" - hd = "hashicorp.com" - redirect_uri = "http://localhost:8000/torii/redirect.html" - } -} - -// indexer contains the configuration for the indexer. -indexer { - // max_parallel_docs is the maximum number of documents that will be - // simultaneously indexed. - max_parallel_docs = 5 - - // update_doc_headers enables the indexer to automatically update document - // headers for changed documents based on Hermes metadata. - update_doc_headers = true - - // update_draft_headers enables the indexer to automatically update document - // headers for draft documents based on Hermes metadata. - update_draft_headers = true -} - -// okta configures Hermes to authenticate users using an AWS Application Load -// Balancer and Okta. -okta { - // auth_server_url is the URL of the Okta authorization server. - auth_server_url = "" - - // ClientID is the Okta client ID. - client_id = "" - - // disabled disables Okta authorization. - disabled = true -} - -// postgres configures PostgreSQL as the app database. -postgres { - dbname = "db" - host = "localhost" - password = "postgres" - port = 5432 - user = "postgres" -} - -// products should be modified to reflect the products/areas in your -// organization. -products { - product "Engineering" { - abbreviation = "ENG" - } - product "Labs" { - abbreviation = "LAB" - } - product "MyProduct" { - abbreviation = "MY" - } -} - -// server contains the configuration for the server. -server { - // addr is the address to bind to for listening. - addr = "127.0.0.1:8000" -} +// // base_url is the base URL used for building links. This should be the public +// // URL of the application. +// base_url = "http://localhost:8000" + +// // algolia configures Hermes to work with Algolia. +// algolia { +// application_id = "" +// docs_index_name = "docs" +// drafts_index_name = "drafts" +// internal_index_name = "internal" +// links_index_name = "links" +// missing_fields_index_name = "missing_fields" +// search_api_key = "" +// write_api_key = "" +// } + +// // document_types configures document types. Currently this block should not be +// // modified, but Hermes will support custom document types in the near future. +// // *** DO NOT MODIFY document_types *** +// document_types { +// document_type "RFC" { +// long_name = "Request for Comments" +// description = "Create a Request for Comments document to present a proposal to colleagues for their review and feedback." +// template = "1Oz_7FhaWxdFUDEzKCC5Cy58t57C4znmC_Qr80BORy1U" + +// more_info_link { +// text = "More info on the RFC template" +// url = "https://works.hashicorp.com/articles/rfc-template" +// } + +// custom_field { +// name = "Current Version" +// type = "string" +// } +// custom_field { +// name = "PRD" +// type = "string" +// } +// custom_field { +// name = "Stakeholders" +// type = "people" +// } +// custom_field { +// name = "Target Version" +// type = "string" +// } +// } + +// document_type "PRD" { +// long_name = "Product Requirements" +// description = "Create a Product Requirements Document to summarize a problem statement and outline a phased approach to addressing the problem." +// template = "1oS4q6IPDr3aMSTTk9UDdOnEcFwVWW9kT8ePCNqcg1P4" + +// more_info_link { +// text = "More info on the PRD template" +// url = "https://works.hashicorp.com/articles/prd-template" +// } + +// custom_field { +// name = "RFC" +// type = "string" +// } +// custom_field { +// name = "Stakeholders" +// type = "people" +// } +// } +// } + +// // email configures Hermes to send email notifications. +// email { +// // enabled enables sending email notifications. +// enabled = true + +// // from_address is the email address to send email notifications from. +// from_address = "hermes@yourorganization.com" +// } + +// // google_workspace configures Hermes to work with Google Workspace. +// google_workspace { +// // create_doc_shortcuts enables creating a shortcut in the shortcuts_folder +// // when a document is published. +// create_doc_shortcuts = true + +// // docs_folder contains all published documents in a flat structure. +// docs_folder = "my-docs-folder-id" + +// // drafts_folder contains all draft documents. +// drafts_folder = "my-drafts-folder-id" + +// // If create_doc_shortcuts is set to true, shortcuts_folder will contain an +// // organized hierarchy of folders and shortcuts to published files that can be +// // easily browsed directly in Google Drive: +// // {shortcut_folder}/{doc_type}/{product}/{document} +// shortcuts_folder = "my-shortcuts-folder-id" + +// // auth is the configuration for interacting with Google Workspace using a +// // service account. +// // auth { +// // client_email = "" +// // private_key = "" +// // subject = "" +// // token_url = "https://oauth2.googleapis.com/token" +// // } + +// // oauth2 is the configuration used to authenticate users via Google. +// oauth2 { +// client_id = "" +// hd = "hashicorp.com" +// redirect_uri = "http://localhost:8000/torii/redirect.html" +// } +// } + +// // indexer contains the configuration for the indexer. +// indexer { +// // max_parallel_docs is the maximum number of documents that will be +// // simultaneously indexed. +// max_parallel_docs = 5 + +// // update_doc_headers enables the indexer to automatically update document +// // headers for changed documents based on Hermes metadata. +// update_doc_headers = true + +// // update_draft_headers enables the indexer to automatically update document +// // headers for draft documents based on Hermes metadata. +// update_draft_headers = true +// } + +// // okta configures Hermes to authenticate users using an AWS Application Load +// // Balancer and Okta. +// okta { +// // auth_server_url is the URL of the Okta authorization server. +// auth_server_url = "" + +// // ClientID is the Okta client ID. +// client_id = "" + +// // disabled disables Okta authorization. +// disabled = true +// } + +// // postgres configures PostgreSQL as the app database. +// postgres { +// dbname = "db" +// host = "localhost" +// password = "postgres" +// port = 5432 +// user = "postgres" +// } + +// // products should be modified to reflect the products/areas in your +// // organization. +// products { +// product "Engineering" { +// abbreviation = "ENG" +// } +// product "Labs" { +// abbreviation = "LAB" +// } +// product "MyProduct" { +// abbreviation = "MY" +// } +// } + +// // server contains the configuration for the server. +// server { +// // addr is the address to bind to for listening. +// addr = "127.0.0.1:8000" +// } diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 0f212c0fc..402452dfb 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -134,7 +134,7 @@ class="hds-typography-body-100 hds-foreground-faint" >Product/Area {{#if this.isDraft}} - + {{yield this to="anchor"}} + + {{#if this.contentIsShown}} +
+ {{yield this to="content"}} +
+ {{/if}} +

diff --git a/web/app/components/floating-u-i.ts b/web/app/components/floating-u-i.ts new file mode 100644 index 000000000..63b44e153 --- /dev/null +++ b/web/app/components/floating-u-i.ts @@ -0,0 +1,24 @@ +import { action } from "@ember/object"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; + +interface FloatingUIComponentSignature { + Args: {}; +} + +export default class FloatingUIComponent extends Component { + @tracked anchor: HTMLElement | null = null; + @tracked contentIsShown: boolean = false; + + @action registerAnchor(e: HTMLElement) { + this.anchor = e; + } + + @action toggleContent() { + this.contentIsShown = !this.contentIsShown; + } + + @action hideContent() { + this.contentIsShown = false; + } +} diff --git a/web/app/components/x/hds/popover.hbs b/web/app/components/x/hds/popover/content.hbs similarity index 100% rename from web/app/components/x/hds/popover.hbs rename to web/app/components/x/hds/popover/content.hbs diff --git a/web/app/components/x/hds/popover.ts b/web/app/components/x/hds/popover/content.ts similarity index 100% rename from web/app/components/x/hds/popover.ts rename to web/app/components/x/hds/popover/content.ts diff --git a/web/app/components/x/hds/popover/index.hbs b/web/app/components/x/hds/popover/index.hbs new file mode 100644 index 000000000..05c2e6f4d --- /dev/null +++ b/web/app/components/x/hds/popover/index.hbs @@ -0,0 +1,43 @@ + +
+ + +
+
+ +{{#if this.popoverIsShown}} + +{{/if}} + +{{#if this.popoverIsShown}} + + + + Toggle Content + + + +
+ This is the content +
+
+
+{{/if}} diff --git a/web/app/components/x/hds/product-badge-dropdown.ts b/web/app/components/x/hds/popover/index.ts similarity index 84% rename from web/app/components/x/hds/product-badge-dropdown.ts rename to web/app/components/x/hds/popover/index.ts index a4133a82a..7724ee09b 100644 --- a/web/app/components/x/hds/product-badge-dropdown.ts +++ b/web/app/components/x/hds/popover/index.ts @@ -27,7 +27,6 @@ export default class XHdsBadgeDropdown extends Component 1; + @action onSelect(product: string) { + this.args.onChange(product); + this.hidePopover(); } @action didInsertTrigger(e: HTMLElement) { @@ -76,13 +69,9 @@ export default class XHdsBadgeDropdown extends Component + + +
+ {{@value}} +
+
+ diff --git a/web/app/components/x/hds/popover/list-item.ts b/web/app/components/x/hds/popover/list-item.ts new file mode 100644 index 000000000..9de65f6e0 --- /dev/null +++ b/web/app/components/x/hds/popover/list-item.ts @@ -0,0 +1,7 @@ +import Component from "@glimmer/component"; + +interface XHdsProductBadgeDropdownListSignature { + Args: {}; +} + +export default class XHdsProductBadgeDropdownList extends Component {} diff --git a/web/app/components/x/hds/popover/list.hbs b/web/app/components/x/hds/popover/list.hbs new file mode 100644 index 000000000..69b819eb3 --- /dev/null +++ b/web/app/components/x/hds/popover/list.hbs @@ -0,0 +1,52 @@ + +
+ {{#if this.inputIsShown}} +
+ +
+ {{/if}} +
+ {{#if this.noMatchesFound}} +
+ No matches +
+ {{else}} +
    + {{#each-in this.shownProducts as |product|}} + + {{/each-in}} +
+ {{/if}} +
+
+
diff --git a/web/app/components/x/hds/popover/list.ts b/web/app/components/x/hds/popover/list.ts new file mode 100644 index 000000000..36b6fc3df --- /dev/null +++ b/web/app/components/x/hds/popover/list.ts @@ -0,0 +1,29 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; + +interface XHdsPopoverListSignature { + Args: { + items: string[]; + resetFocusedItemIndex: () => void; + }; +} + +export default class XHdsPopoverList extends Component { + @tracked _input: HTMLInputElement | null = null; + + get inputIsShown() { + return Object.keys(this.args.items).length > 7; + } + + get input() { + assert("input must exist", this._input); + return this._input; + } + + @action registerAndFocusInput(e: HTMLInputElement) { + this._input = e; + this.input.focus(); + } +} diff --git a/web/app/components/x/hds/product-badge-dropdown.hbs b/web/app/components/x/hds/product-badge-dropdown.hbs deleted file mode 100644 index f962b13f1..000000000 --- a/web/app/components/x/hds/product-badge-dropdown.hbs +++ /dev/null @@ -1,61 +0,0 @@ -{{#if @readOnly}} - -{{else}} - -
- - -
-
- {{#if this.popoverIsShown}} - -
- {{! TODO: Make this dependent on the number of products }} - {{#if this.inputIsShown}} -
- -
- {{/if}} -
    - {{#each-in this.shownProducts as |product|}} -
  1. {{product}}
  2. - {{/each-in}} -
-
-
- {{/if}} -{{/if}} diff --git a/web/app/router.js b/web/app/router.js index 9c7f6cdd1..c8a3612aa 100644 --- a/web/app/router.js +++ b/web/app/router.js @@ -20,5 +20,6 @@ Router.map(function () { }); }); this.route("authenticate"); - this.route('404', { path: '/*path' }) + this.route("playground"); + this.route("404", { path: "/*path" }); }); diff --git a/web/app/styles/components/popover.scss b/web/app/styles/components/popover.scss index 1e38f841f..be5d2bd6c 100644 --- a/web/app/styles/components/popover.scss +++ b/web/app/styles/components/popover.scss @@ -1,5 +1,6 @@ .hermes-popover { - @apply absolute bg-color-foreground-high-contrast rounded z-50 hds-dropdown-list; + @apply absolute bg-color-foreground-high-contrast rounded z-50 hds-dropdown__content; + // hermes dropdown list styles /* These positioning styles are overwritten by FloatingUI, * but they ensure that the tooltip isn't added to the bottom of the page, diff --git a/web/app/templates/playground.hbs b/web/app/templates/playground.hbs new file mode 100644 index 000000000..775766d3c --- /dev/null +++ b/web/app/templates/playground.hbs @@ -0,0 +1,13 @@ +Playground + + <:anchor as |f|> + + Toggle Content + + + <:content as |f|> +
+ This is the content +
+ +
From 7d95f39bac8197c6632b4c231386705a1710d450 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 14:30:41 -0400 Subject: [PATCH 005/128] Organization --- web/app/components/{x/hds/popover => floating-u-i}/content.hbs | 0 web/app/components/{x/hds/popover => floating-u-i}/content.ts | 0 web/app/components/{floating-u-i.hbs => floating-u-i/index.hbs} | 0 web/app/components/{floating-u-i.ts => floating-u-i/index.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename web/app/components/{x/hds/popover => floating-u-i}/content.hbs (100%) rename web/app/components/{x/hds/popover => floating-u-i}/content.ts (100%) rename web/app/components/{floating-u-i.hbs => floating-u-i/index.hbs} (100%) rename web/app/components/{floating-u-i.ts => floating-u-i/index.ts} (100%) diff --git a/web/app/components/x/hds/popover/content.hbs b/web/app/components/floating-u-i/content.hbs similarity index 100% rename from web/app/components/x/hds/popover/content.hbs rename to web/app/components/floating-u-i/content.hbs diff --git a/web/app/components/x/hds/popover/content.ts b/web/app/components/floating-u-i/content.ts similarity index 100% rename from web/app/components/x/hds/popover/content.ts rename to web/app/components/floating-u-i/content.ts diff --git a/web/app/components/floating-u-i.hbs b/web/app/components/floating-u-i/index.hbs similarity index 100% rename from web/app/components/floating-u-i.hbs rename to web/app/components/floating-u-i/index.hbs diff --git a/web/app/components/floating-u-i.ts b/web/app/components/floating-u-i/index.ts similarity index 100% rename from web/app/components/floating-u-i.ts rename to web/app/components/floating-u-i/index.ts From 86e5b2cc062b6ea9afca12209443ec654635108d Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 14:39:14 -0400 Subject: [PATCH 006/128] Maybe In Element --- web/app/components/floating-u-i/content.hbs | 16 ++-------------- web/app/components/floating-u-i/content.ts | 4 ++-- web/package.json | 1 + web/yarn.lock | 14 +++++++++++++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index 008ad81ad..a6c97d184 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -1,17 +1,5 @@ -{{#if @renderOut}} - {{#in-element (html-element ".ember-application") insertBefore=null}} +{{#maybe-in-element (html-element '.ember-application') renderInPlace=this.shouldRenderInPlace insertBefore}}
- {{yield}} -
- {{/in-element}} -{{else}} -
{{yield}}
-{{/if}} +{{/maybe-in-element}} diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 939268c8d..333a13f3c 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -14,7 +14,7 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import htmlElement from "hermes/utils/html-element"; -interface XHdsPopoverSignature { +interface FloatingUIContentSignature { Args: { anchor: HTMLElement; placement?: Placement; @@ -22,7 +22,7 @@ interface XHdsPopoverSignature { }; } -export default class XHdsPopover extends Component { +export default class FloatingUIContent extends Component { @tracked _popover: HTMLElement | null = null; get id() { diff --git a/web/package.json b/web/package.json index 1a41829f6..963d066f0 100644 --- a/web/package.json +++ b/web/package.json @@ -82,6 +82,7 @@ "ember-focus-trap": "^1.0.1", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^0.1.6", + "ember-maybe-in-element": "^2.1.0", "ember-modifier": "^4.1.0", "ember-on-helper": "^0.1.0", "ember-page-title": "^6.2.2", diff --git a/web/yarn.lock b/web/yarn.lock index 203d0d2f0..44de1611e 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -7815,7 +7815,7 @@ __metadata: languageName: node linkType: hard -"ember-cli-htmlbars@npm:^6.2.0": +"ember-cli-htmlbars@npm:^6.1.1, ember-cli-htmlbars@npm:^6.2.0": version: 6.2.0 resolution: "ember-cli-htmlbars@npm:6.2.0" dependencies: @@ -8531,6 +8531,17 @@ __metadata: languageName: node linkType: hard +"ember-maybe-in-element@npm:^2.1.0": + version: 2.1.0 + resolution: "ember-maybe-in-element@npm:2.1.0" + dependencies: + ember-cli-babel: ^7.26.11 + ember-cli-htmlbars: ^6.1.1 + ember-cli-version-checker: ^5.1.2 + checksum: fbe6282329048c3b811c6f6df9acb821dcc085de96f5cf28135ed3e46186b72f0ce198a53ed3c3a451308eb3f77983c68db97ee1d0d406deda77faef1a89d777 + languageName: node + linkType: hard + "ember-modifier-manager-polyfill@npm:^1.2.0": version: 1.2.0 resolution: "ember-modifier-manager-polyfill@npm:1.2.0" @@ -10977,6 +10988,7 @@ __metadata: ember-focus-trap: ^1.0.1 ember-load-initializers: ^2.1.2 ember-maybe-import-regenerator: ^0.1.6 + ember-maybe-in-element: ^2.1.0 ember-modifier: ^4.1.0 ember-on-helper: ^0.1.0 ember-page-title: ^6.2.2 From 7b9a5ead21ae1dca1a9d6b52eb8818de0cae3b3c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 14:49:48 -0400 Subject: [PATCH 007/128] More API development --- web/app/components/floating-u-i/content.hbs | 9 +++++-- web/app/components/floating-u-i/content.ts | 8 ++++++ web/app/components/floating-u-i/index.hbs | 28 ++++++++------------- web/app/components/floating-u-i/index.ts | 26 ++++++++++++++++--- web/app/templates/playground.hbs | 4 +-- 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index a6c97d184..ccc9d8b9a 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -1,7 +1,12 @@ -{{#maybe-in-element (html-element '.ember-application') renderInPlace=this.shouldRenderInPlace insertBefore}} -
{ @tracked _popover: HTMLElement | null = null; + get shouldRenderInPlace() { + if (this.args.renderOut) { + return false; + } else { + return true; + } + } + get id() { return guidFor(this); } diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index a6261588f..aa82faba0 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -1,18 +1,12 @@ -{{! yield an Anchor block }} -{{! yield a Content block }} -{{! yield `toggleContent`}} -{{! yield `hideContent`}} -{{! yield `showContent`}} -{{! yield isShown, etc }} +{{yield this to="anchor"}} -{{! accept placement options }} - -
- {{yield this to="anchor"}} - - {{#if this.contentIsShown}} -
- {{yield this to="content"}} -
- {{/if}} -
+{{#if this.contentIsShown}} + + {{yield this to="content"}} + +{{/if}} diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 63b44e153..1993c5a77 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -1,21 +1,39 @@ +import { assert } from "@ember/debug"; import { action } from "@ember/object"; +import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; interface FloatingUIComponentSignature { - Args: {}; + Args: { + renderOut?: boolean; + placement?: Placement; + }; } export default class FloatingUIComponent extends Component { - @tracked anchor: HTMLElement | null = null; + @tracked _anchor: HTMLElement | null = null; @tracked contentIsShown: boolean = false; @action registerAnchor(e: HTMLElement) { - this.anchor = e; + this._anchor = e; + } + + get anchor() { + assert("_anchor must exist", this._anchor); + return this._anchor; } @action toggleContent() { - this.contentIsShown = !this.contentIsShown; + if (this.contentIsShown) { + this.hideContent(); + } else { + this.showContent(); + } + } + + @action showContent() { + this.contentIsShown = true; } @action hideContent() { diff --git a/web/app/templates/playground.hbs b/web/app/templates/playground.hbs index 775766d3c..9ff46fdec 100644 --- a/web/app/templates/playground.hbs +++ b/web/app/templates/playground.hbs @@ -1,7 +1,7 @@ Playground - + <:anchor as |f|> - + Toggle Content From 3f09a0750718a1c6cf2f6a5cc0f44f0a24781e0f Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 17:22:55 -0400 Subject: [PATCH 008/128] Fix type errors --- web/app/components/document/sidebar.hbs | 34 +---- .../header/facet-dropdown-list-item.ts | 2 +- .../components/header/facet-dropdown-list.ts | 2 +- web/app/components/header/facet-dropdown.ts | 8 +- web/app/components/inputs/product-select.hbs | 32 +++++ web/app/components/inputs/product-select.ts | 45 +++++++ .../x/hds/{popover => dropdown}/index.hbs | 36 +++--- web/app/components/x/hds/dropdown/index.ts | 116 ++++++++++++++++++ .../x/hds/{popover => dropdown}/list-item.hbs | 0 .../x/hds/{popover => dropdown}/list-item.ts | 0 web/app/components/x/hds/dropdown/list.hbs | 45 +++++++ .../x/hds/{popover => dropdown}/list.ts | 11 +- web/app/components/x/hds/popover/index.ts | 98 --------------- web/app/components/x/hds/popover/list.hbs | 52 -------- .../header/facet-dropdown-list-test.ts | 6 +- 15 files changed, 278 insertions(+), 209 deletions(-) create mode 100644 web/app/components/inputs/product-select.hbs create mode 100644 web/app/components/inputs/product-select.ts rename web/app/components/x/hds/{popover => dropdown}/index.hbs (50%) create mode 100644 web/app/components/x/hds/dropdown/index.ts rename web/app/components/x/hds/{popover => dropdown}/list-item.hbs (100%) rename web/app/components/x/hds/{popover => dropdown}/list-item.ts (100%) create mode 100644 web/app/components/x/hds/dropdown/list.hbs rename web/app/components/x/hds/{popover => dropdown}/list.ts (67%) delete mode 100644 web/app/components/x/hds/popover/index.ts delete mode 100644 web/app/components/x/hds/popover/list.hbs diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 402452dfb..a4ef2b836 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -134,38 +134,10 @@ class="hds-typography-body-100 hds-foreground-faint" >Product/Area {{#if this.isDraft}} - - {{else}} { @tracked private _triggerElement: HTMLButtonElement | null = null; @tracked private _scrollContainer: HTMLElement | null = null; diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs new file mode 100644 index 000000000..e8f6e9b73 --- /dev/null +++ b/web/app/components/inputs/product-select.hbs @@ -0,0 +1,32 @@ +{{! This could be replaced with, e.g., }} + + <:anchor as |f|> + {{! how can i "tag" this as keyboard/mouse-navigable }} + +
+ + +
+
+ + <:content as |f|> + + +
diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts new file mode 100644 index 000000000..0f59716f2 --- /dev/null +++ b/web/app/components/inputs/product-select.ts @@ -0,0 +1,45 @@ +import { assert } from "@ember/debug"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { task } from "ember-concurrency"; +import FetchService from "hermes/services/fetch"; + +interface InputsProductSelectSignatureSignature { + Element: null; + Args: {}; + Blocks: { + default: []; + }; +} + +type ProductAreas = { + [key: string]: { + abbreviation: string; + perDocDataType: unknown; + }; +}; + +export default class InputsProductSelectSignature extends Component { + @tracked _products: ProductAreas | undefined = undefined; + @tracked shownProducts: ProductAreas | null = null; + + get products() { + assert("_products must exist", this._products); + return this._products; + } + + @service("fetch") declare fetchSvc: FetchService; + protected fetchProducts = task(async () => { + try { + let products = await this.fetchSvc + .fetch("/api/v1/products") + .then((resp) => resp?.json()); + this._products = products; + } catch (err) { + console.error(err); + throw err; + } + }); + +} diff --git a/web/app/components/x/hds/popover/index.hbs b/web/app/components/x/hds/dropdown/index.hbs similarity index 50% rename from web/app/components/x/hds/popover/index.hbs rename to web/app/components/x/hds/dropdown/index.hbs index 05c2e6f4d..0671e41e1 100644 --- a/web/app/components/x/hds/popover/index.hbs +++ b/web/app/components/x/hds/dropdown/index.hbs @@ -27,17 +27,25 @@ /> {{/if}} -{{#if this.popoverIsShown}} - - - - Toggle Content - - - -
- This is the content -
-
-
-{{/if}} + + <:anchor as |f|> + + {{yield f to="anchor"}} + + + <:content as |f|> + + + diff --git a/web/app/components/x/hds/dropdown/index.ts b/web/app/components/x/hds/dropdown/index.ts new file mode 100644 index 000000000..cbf5a6f8c --- /dev/null +++ b/web/app/components/x/hds/dropdown/index.ts @@ -0,0 +1,116 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import { schedule } from "@ember/runloop"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { restartableTask, task } from "ember-concurrency"; +import FetchService from "hermes/services/fetch"; + +interface XHdsDropdownComponentSignature { + Args: { + selected: any; + items: any; + onChange: (value: any) => void; + }; +} + +export enum FocusDirection { + Previous = "previous", + Next = "next", + First = "first", + Last = "last", +} + +export default class XHdsDropdownComponent extends Component< + XHdsDropdownComponentSignature +> { + @service("fetch") declare fetchSvc: FetchService; + + @tracked _trigger: HTMLElement | null = null; + @tracked _items: unknown | undefined = undefined; + @tracked private _scrollContainer: HTMLElement | null = null; + + @tracked protected focusedItemIndex = -1; + + @tracked filteredItems: unknown | null = null; + + /** + * The dropdown menu items. Registered on insert and + * updated with on keydown and filterInput events. + * Used to determine the list length, and to find the focused + * element by index. + */ + @tracked protected menuItems: NodeListOf | null = null; + + /** + * An asserted-true reference to the scroll container. + * Used in the `maybeScrollIntoView` calculations. + */ + private get scrollContainer(): HTMLElement { + assert("_scrollContainer must exist", this._scrollContainer); + return this._scrollContainer; + } + + get shownItems() { + return this.filteredItems || this.items; + } + + get items() { + assert("products must exist", this._items); + return this._items; + } + + @action willDestroyDropdown() { + this.filteredItems = null; + } + + @action onSelect(product: string, hideDropdown: () => void) { + this.args.onChange(product); + hideDropdown(); + } + + /** + * The action run when the popover is inserted, and when + * the user filters or navigates the dropdown. + * Loops through the menu items and assigns an id that + * matches the index of the item in the list. + */ + @action assignMenuItemIDs(items: NodeListOf): void { + this.menuItems = items; + for (let i = 0; i < items.length; i++) { + let item = items[i]; + assert("item must exist", item instanceof HTMLElement); + item.id = `facet-dropdown-menu-item-${i}`; + } + } + + protected onInput = restartableTask( + async (inputEvent: InputEvent, f: any) => { + this.focusedItemIndex = -1; + + + // TODO: type the API interface + + // need some handling whether it's an object or an array + + // let shownFacets: FacetDropdownObjects = {}; + // let facets = this.args.facets; + + // this.query = (inputEvent.target as HTMLInputElement).value; + // for (const [key, value] of Object.entries(facets)) { + // if (key.toLowerCase().includes(this.query.toLowerCase())) { + // shownFacets[key] = value; + // } + // } + + // this.filteredItems = shownFacets; + + // schedule("afterRender", () => { + // this.assignMenuItemIDs( + // f.content.querySelectorAll(`[role=${this.listItemRole}]`) + // ); + // }); + } + ); +} diff --git a/web/app/components/x/hds/popover/list-item.hbs b/web/app/components/x/hds/dropdown/list-item.hbs similarity index 100% rename from web/app/components/x/hds/popover/list-item.hbs rename to web/app/components/x/hds/dropdown/list-item.hbs diff --git a/web/app/components/x/hds/popover/list-item.ts b/web/app/components/x/hds/dropdown/list-item.ts similarity index 100% rename from web/app/components/x/hds/popover/list-item.ts rename to web/app/components/x/hds/dropdown/list-item.ts diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs new file mode 100644 index 000000000..5c0ae4d24 --- /dev/null +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -0,0 +1,45 @@ +{{on-document "keydown" this.maybeKeyboardNavigate}} + +
+ {{#if this.inputIsShown}} +
+ +
+ {{/if}} +
+ {{#if this.noMatchesFound}} +
+ No matches +
+ {{else}} +
    + {{#each-in @shownProducts as |product|}} + + {{/each-in}} +
+ {{/if}} +
+
diff --git a/web/app/components/x/hds/popover/list.ts b/web/app/components/x/hds/dropdown/list.ts similarity index 67% rename from web/app/components/x/hds/popover/list.ts rename to web/app/components/x/hds/dropdown/list.ts index 36b6fc3df..9eadd4734 100644 --- a/web/app/components/x/hds/popover/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -3,14 +3,19 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; -interface XHdsPopoverListSignature { +interface XHdsDropdownListSignature { Args: { - items: string[]; + items: any; + isOrdered?: boolean; + onChange: (e: Event) => void; resetFocusedItemIndex: () => void; + registerScrollContainer?: (e: HTMLElement) => void; }; } -export default class XHdsPopoverList extends Component { +export default class XHdsDropdownList extends Component< + XHdsDropdownListSignature +> { @tracked _input: HTMLInputElement | null = null; get inputIsShown() { diff --git a/web/app/components/x/hds/popover/index.ts b/web/app/components/x/hds/popover/index.ts deleted file mode 100644 index 7724ee09b..000000000 --- a/web/app/components/x/hds/popover/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { assert } from "@ember/debug"; -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { task } from "ember-concurrency"; -import FetchService from "hermes/services/fetch"; - -interface XHdsBadgeDropdownSignature { - Args: { - currentProduct: string; - options: string[]; - onChange: (value: string) => void; - readonly: boolean; - }; -} - -type ProductAreas = { - [key: string]: { - abbreviation: string; - perDocDataType: unknown; - }; -}; - -export default class XHdsBadgeDropdown extends Component { - @service("fetch") declare fetchSvc: FetchService; - - @tracked _trigger: HTMLElement | null = null; - @tracked _products: ProductAreas | undefined = undefined; - - @tracked popoverIsShown = false; - - @tracked filteredProducts: ProductAreas | null = null; - - get shownProducts() { - return this.filteredProducts || this.products; - } - - get trigger() { - assert("trigger must exist", this._trigger); - return this._trigger; - } - - get products() { - assert("products must exist", this._products); - return this._products; - } - - @action togglePopover() { - if (this.popoverIsShown) { - this.hidePopover(); - } else { - this.popoverIsShown = true; - } - } - - @action hidePopover() { - this.popoverIsShown = false; - this.filteredProducts = null; - } - - @action onSelect(product: string) { - this.args.onChange(product); - this.hidePopover(); - } - - @action didInsertTrigger(e: HTMLElement) { - this._trigger = e; - void this.fetchProducts.perform(); - } - - @action onInput(e: InputEvent) { - // filter the products by the input value - // @ts-ignore - let value = this.input.value; - if (value) { - this.filteredProducts = Object.fromEntries( - Object.entries(this.products).filter(([key]) => - key.toLowerCase().includes(value.toLowerCase()) - ) - ); - } else { - this.filteredProducts = null; - } - } - - protected fetchProducts = task(async () => { - try { - let products = await this.fetchSvc - .fetch("/api/v1/products") - .then((resp) => resp?.json()); - this._products = products; - } catch (err) { - console.error(err); - throw err; - } - }); -} diff --git a/web/app/components/x/hds/popover/list.hbs b/web/app/components/x/hds/popover/list.hbs deleted file mode 100644 index 69b819eb3..000000000 --- a/web/app/components/x/hds/popover/list.hbs +++ /dev/null @@ -1,52 +0,0 @@ - -
- {{#if this.inputIsShown}} -
- -
- {{/if}} -
- {{#if this.noMatchesFound}} -
- No matches -
- {{else}} -
    - {{#each-in this.shownProducts as |product|}} - - {{/each-in}} -
- {{/if}} -
-
-
diff --git a/web/tests/integration/components/header/facet-dropdown-list-test.ts b/web/tests/integration/components/header/facet-dropdown-list-test.ts index 173b4c47f..7b0b1962a 100644 --- a/web/tests/integration/components/header/facet-dropdown-list-test.ts +++ b/web/tests/integration/components/header/facet-dropdown-list-test.ts @@ -4,7 +4,7 @@ import { find, findAll, render, triggerKeyEvent } from "@ember/test-helpers"; import { assert as emberAssert } from "@ember/debug"; import { hbs } from "ember-cli-htmlbars"; import { LONG_FACET_LIST, SHORT_FACET_LIST } from "./facet-dropdown-test"; -import { FocusDirection } from "hermes/components/header/facet-dropdown"; +import { FocusDirection } from "hermes/components/x/hds/dropdown"; module( "Integration | Component | header/facet-dropdown-list", @@ -83,7 +83,9 @@ module( emberAssert("input must exist", input); assert.equal(document.activeElement, input, "The input is autofocused"); - assert.dom("[data-test-facet-dropdown-popover]").hasAttribute("role", "combobox"); + assert + .dom("[data-test-facet-dropdown-popover]") + .hasAttribute("role", "combobox"); assert .dom(inputSelector) .doesNotHaveAttribute( From cb2d2808d15ab80e4182a3e33f4b1189d5ce9d37 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 18:07:10 -0400 Subject: [PATCH 009/128] Additional component work --- web/app/components/inputs/product-select.hbs | 41 +++--- web/app/components/x/hds/dropdown/index.hbs | 18 ++- web/app/components/x/hds/dropdown/index.ts | 130 ++++++++++++++++--- web/app/components/x/hds/dropdown/list.hbs | 17 +-- web/app/components/x/hds/dropdown/list.ts | 34 +++++ 5 files changed, 192 insertions(+), 48 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index e8f6e9b73..f1b866177 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,24 +1,9 @@ {{! This could be replaced with, e.g., }} + + + <:toggleButton as |f|> +
+ + +
+ +
diff --git a/web/app/components/x/hds/dropdown/index.hbs b/web/app/components/x/hds/dropdown/index.hbs index 0671e41e1..7806b16a1 100644 --- a/web/app/components/x/hds/dropdown/index.hbs +++ b/web/app/components/x/hds/dropdown/index.hbs @@ -1,3 +1,4 @@ + <:anchor as |f|> - {{yield f to="anchor"}} + {{yield f to="toggleButton"}} + <:content as |f|> diff --git a/web/app/components/x/hds/dropdown/index.ts b/web/app/components/x/hds/dropdown/index.ts index cbf5a6f8c..76a33989d 100644 --- a/web/app/components/x/hds/dropdown/index.ts +++ b/web/app/components/x/hds/dropdown/index.ts @@ -12,6 +12,7 @@ interface XHdsDropdownComponentSignature { selected: any; items: any; onChange: (value: any) => void; + listIsOrdered?: boolean; }; } @@ -34,19 +35,8 @@ export default class XHdsDropdownComponent extends Component< @tracked protected focusedItemIndex = -1; @tracked filteredItems: unknown | null = null; - - /** - * The dropdown menu items. Registered on insert and - * updated with on keydown and filterInput events. - * Used to determine the list length, and to find the focused - * element by index. - */ @tracked protected menuItems: NodeListOf | null = null; - /** - * An asserted-true reference to the scroll container. - * Used in the `maybeScrollIntoView` calculations. - */ private get scrollContainer(): HTMLElement { assert("_scrollContainer must exist", this._scrollContainer); return this._scrollContainer; @@ -61,6 +51,10 @@ export default class XHdsDropdownComponent extends Component< return this._items; } + @action protected registerScrollContainer(element: HTMLDivElement) { + this._scrollContainer = element; + } + @action willDestroyDropdown() { this.filteredItems = null; } @@ -70,12 +64,6 @@ export default class XHdsDropdownComponent extends Component< hideDropdown(); } - /** - * The action run when the popover is inserted, and when - * the user filters or navigates the dropdown. - * Loops through the menu items and assigns an id that - * matches the index of the item in the list. - */ @action assignMenuItemIDs(items: NodeListOf): void { this.menuItems = items; for (let i = 0; i < items.length; i++) { @@ -85,11 +73,117 @@ export default class XHdsDropdownComponent extends Component< } } + @action protected setFocusedItemIndex( + focusDirectionOrNumber: FocusDirection | number, + maybeScrollIntoView = true + ) { + let { menuItems, focusedItemIndex } = this; + + let setFirst = () => { + focusedItemIndex = 0; + }; + + let setLast = () => { + assert("menuItems must exist", menuItems); + focusedItemIndex = menuItems.length - 1; + }; + + if (!menuItems) { + return; + } + + if (menuItems.length === 0) { + return; + } + + switch (focusDirectionOrNumber) { + case FocusDirection.Previous: + if (focusedItemIndex === -1 || focusedItemIndex === 0) { + // When the first or no item is focused, "previous" focuses the last item. + setLast(); + } else { + focusedItemIndex--; + } + break; + case FocusDirection.Next: + if (focusedItemIndex === menuItems.length - 1) { + // When the last item is focused, "next" focuses the first item. + setFirst(); + } else { + focusedItemIndex++; + } + break; + case FocusDirection.First: + setFirst(); + break; + case FocusDirection.Last: + setLast(); + break; + default: + focusedItemIndex = focusDirectionOrNumber; + break; + } + + this.focusedItemIndex = focusedItemIndex; + + if (maybeScrollIntoView) { + this.maybeScrollIntoView(); + } + } + + @action protected resetFocusedItemIndex() { + this.focusedItemIndex = -1; + } + + private maybeScrollIntoView() { + const focusedItem = this.menuItems?.item(this.focusedItemIndex); + assert("focusedItem must exist", focusedItem instanceof HTMLElement); + + const containerTopPadding = 12; + const containerHeight = this.scrollContainer.offsetHeight; + const itemHeight = focusedItem.offsetHeight; + const itemTop = focusedItem.offsetTop; + const itemBottom = focusedItem.offsetTop + itemHeight; + const scrollviewTop = this.scrollContainer.scrollTop - containerTopPadding; + const scrollviewBottom = scrollviewTop + containerHeight; + + if (itemBottom > scrollviewBottom) { + this.scrollContainer.scrollTop = itemTop + itemHeight - containerHeight; + } else if (itemTop < scrollviewTop) { + this.scrollContainer.scrollTop = itemTop; + } + } + + @action protected onTriggerKeydown(event: KeyboardEvent, f: any) { + if (f.contentIsShown) { + return; + } + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault(); + f.hideDropdown(); + + // Stop the event from bubbling to the popover's keydown handler. + event.stopPropagation(); + + // Wait for the menuItems to be set by the showDropdown action. + schedule("afterRender", () => { + switch (event.key) { + case "ArrowDown": + this.setFocusedItemIndex(FocusDirection.First, false); + break; + case "ArrowUp": + this.setFocusedItemIndex(FocusDirection.Last); + break; + } + }); + } + } + protected onInput = restartableTask( async (inputEvent: InputEvent, f: any) => { this.focusedItemIndex = -1; - // TODO: type the API interface // need some handling whether it's an object or an array diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index 5c0ae4d24..0cc9f6c34 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -1,7 +1,8 @@ -{{on-document "keydown" this.maybeKeyboardNavigate}} +{{on-document "keydown" (fn this.maybeKeyboardNavigate @f)}}
- {{#if this.inputIsShown}} + Nice!! + {{!-- {{#if this.inputIsShown}}
- {{/if}} + {{/if}} --}}
- {{#if this.noMatchesFound}} + {{!-- {{#if this.noMatchesFound}}
{{else}}
    - {{#each-in @shownProducts as |product|}} + {{#each-in @shownItems as |product|}} {{/each-in}}
- {{/if}} + {{/if}} --}}
diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index 9eadd4734..fb3c11a4f 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -2,14 +2,18 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; +import { FocusDirection } from "."; interface XHdsDropdownListSignature { Args: { items: any; + shownItems: any; isOrdered?: boolean; onChange: (e: Event) => void; resetFocusedItemIndex: () => void; registerScrollContainer?: (e: HTMLElement) => void; + setFocusedItemIndex: (direction: FocusDirection) => void; + f: any; }; } @@ -27,8 +31,38 @@ export default class XHdsDropdownList extends Component< return this._input; } + protected get noMatchesFound(): boolean { + if (!this.inputIsShown) { + return false; + } + return Object.entries(this.args.shownItems).length === 0; + } + @action registerAndFocusInput(e: HTMLInputElement) { this._input = e; this.input.focus(); } + + @action protected maybeKeyboardNavigate(event: KeyboardEvent) { + if (event.key === "ArrowDown") { + event.preventDefault(); + this.args.setFocusedItemIndex(FocusDirection.Next); + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + this.args.setFocusedItemIndex(FocusDirection.Previous); + } + + if (event.key === "Enter") { + event.preventDefault(); + assert("popoverElement must exist", this.args.f.content); + const target = this.args.f.content.querySelector("[aria-selected]"); + + if (target instanceof HTMLAnchorElement) { + target.click(); + this.args.f.hideContent(); + } + } + } } From 88ac4bcc6212d140a021b8608ff9e50fc550a824 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 11 Apr 2023 20:24:25 -0400 Subject: [PATCH 010/128] Progress --- web/app/components/inputs/product-select.hbs | 23 +++---------- web/app/components/inputs/product-select.ts | 20 ++++------- web/app/components/x/hds/dropdown/index.ts | 11 ++---- .../components/x/hds/dropdown/list-item.hbs | 2 +- .../components/x/hds/dropdown/list-item.ts | 16 +++++++-- web/app/components/x/hds/dropdown/list.hbs | 34 +++++++++++-------- web/app/components/x/hds/dropdown/list.ts | 3 +- 7 files changed, 50 insertions(+), 59 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index f1b866177..0ca0bea86 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,22 +1,7 @@ -{{! This could be replaced with, e.g., }} - <:toggleButton as |f|> -
+
void; }; } @@ -21,25 +20,20 @@ type ProductAreas = { }; export default class InputsProductSelectSignature extends Component { - @tracked _products: ProductAreas | undefined = undefined; - @tracked shownProducts: ProductAreas | null = null; + @service("fetch") declare fetchSvc: FetchService; - get products() { - assert("_products must exist", this._products); - return this._products; - } + @tracked products: ProductAreas | undefined = undefined; + @tracked shownProducts: ProductAreas | null = null; - @service("fetch") declare fetchSvc: FetchService; protected fetchProducts = task(async () => { try { let products = await this.fetchSvc .fetch("/api/v1/products") .then((resp) => resp?.json()); - this._products = products; + this.products = products; } catch (err) { console.error(err); throw err; } }); - } diff --git a/web/app/components/x/hds/dropdown/index.ts b/web/app/components/x/hds/dropdown/index.ts index 76a33989d..be889df6b 100644 --- a/web/app/components/x/hds/dropdown/index.ts +++ b/web/app/components/x/hds/dropdown/index.ts @@ -10,7 +10,7 @@ import FetchService from "hermes/services/fetch"; interface XHdsDropdownComponentSignature { Args: { selected: any; - items: any; + items?: any; onChange: (value: any) => void; listIsOrdered?: boolean; }; @@ -29,7 +29,6 @@ export default class XHdsDropdownComponent extends Component< @service("fetch") declare fetchSvc: FetchService; @tracked _trigger: HTMLElement | null = null; - @tracked _items: unknown | undefined = undefined; @tracked private _scrollContainer: HTMLElement | null = null; @tracked protected focusedItemIndex = -1; @@ -43,12 +42,8 @@ export default class XHdsDropdownComponent extends Component< } get shownItems() { - return this.filteredItems || this.items; - } - - get items() { - assert("products must exist", this._items); - return this._items; + console.log(this.args.items); + return this.filteredItems || this.args.items; } @action protected registerScrollContainer(element: HTMLDivElement) { diff --git a/web/app/components/x/hds/dropdown/list-item.hbs b/web/app/components/x/hds/dropdown/list-item.hbs index ce9553574..0591cf161 100644 --- a/web/app/components/x/hds/dropdown/list-item.hbs +++ b/web/app/components/x/hds/dropdown/list-item.hbs @@ -1,6 +1,6 @@
  • diff --git a/web/app/components/x/hds/dropdown/list-item.ts b/web/app/components/x/hds/dropdown/list-item.ts index 9de65f6e0..40e997329 100644 --- a/web/app/components/x/hds/dropdown/list-item.ts +++ b/web/app/components/x/hds/dropdown/list-item.ts @@ -1,7 +1,19 @@ +import { action } from "@ember/object"; import Component from "@glimmer/component"; interface XHdsProductBadgeDropdownListSignature { - Args: {}; + Args: { + role?: string; + value?: any; + selected: boolean; + hideDropdown: () => void; + onChange: (value: any) => void; + }; } -export default class XHdsProductBadgeDropdownList extends Component {} +export default class XHdsProductBadgeDropdownList extends Component { + @action onClick() { + this.args.onChange(this.args.value); + this.args.hideDropdown(); + } +} diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index 0cc9f6c34..034c5d0ec 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -2,7 +2,7 @@
    Nice!! - {{!-- {{#if this.inputIsShown}} + {{#if this.inputIsShown}}
    - {{/if}} --}} + {{/if}}
    - {{!-- {{#if this.noMatchesFound}} + {{#if this.noMatchesFound}}
    {{else}} -
      - {{#each-in @shownItems as |product|}} - - {{/each-in}} -
    - {{/if}} --}} + {{#if @listIsOrdered}} +
      + {{#if @shownItems}} + {{#each-in @shownItems as |product|}} + + {{/each-in}} + {{/if}} +
    + {{/if}} + {{/if}}
    diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index fb3c11a4f..db6539c0e 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -6,8 +6,9 @@ import { FocusDirection } from "."; interface XHdsDropdownListSignature { Args: { - items: any; + items?: any; shownItems: any; + selected: any; isOrdered?: boolean; onChange: (e: Event) => void; resetFocusedItemIndex: () => void; From 8e78bacf87543d4655a78f26a4c6a11d5ac1033e Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 12 Apr 2023 12:20:51 -0400 Subject: [PATCH 011/128] Start of keyboard nav hookup --- web/app/components/floating-u-i/content.ts | 22 +++-- web/app/components/floating-u-i/index.hbs | 1 + web/app/components/floating-u-i/index.ts | 6 ++ web/app/components/inputs/product-select.hbs | 1 + web/app/components/x/hds/dropdown/index.hbs | 40 ++------ web/app/components/x/hds/dropdown/index.ts | 58 +++++++----- .../components/x/hds/dropdown/list-item.hbs | 25 ++++- .../components/x/hds/dropdown/list-item.ts | 92 +++++++++++++++++++ web/app/components/x/hds/dropdown/list.hbs | 28 ++++-- web/app/components/x/hds/dropdown/list.ts | 7 +- 10 files changed, 201 insertions(+), 79 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 07ec00a7b..1562e1376 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -23,7 +23,7 @@ interface FloatingUIContentSignature { } export default class FloatingUIContent extends Component { - @tracked _popover: HTMLElement | null = null; + @tracked _content: HTMLElement | null = null; get shouldRenderInPlace() { if (this.args.renderOut) { @@ -37,9 +37,9 @@ export default class FloatingUIContent extends Component void) | null = null; @action didInsert(e: HTMLElement) { - this._popover = e; + this._content = e; let updatePosition = async () => { - computePosition(this.args.anchor, this.popover, { + computePosition(this.args.anchor, this.content, { platform: platform, - placement: this.args.placement || "bottom", + placement: this.args.placement || "bottom-start", middleware: [offset(5), flip(), shift()], }).then(({ x, y, placement }) => { - this.popover.setAttribute("data-popover-placement", placement); + this.content.setAttribute("data-popover-placement", placement); - Object.assign(this.popover.style, { + Object.assign(this.content.style, { left: `${x}px`, top: `${y}px`, }); }); }; - this.cleanup = autoUpdate(this.args.anchor, this.popover, updatePosition); + updatePosition(); + + this.cleanup = autoUpdate(this.args.anchor, this.content, updatePosition); } } diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index aa82faba0..6c89af21c 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -5,6 +5,7 @@ @placement={{@placement}} @renderOut={{@renderOut}} @anchor={{this.anchor}} + {{did-insert this.registerContent}} ...attributes > {{yield this to="content"}} diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 1993c5a77..01daaa8e6 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -13,12 +13,18 @@ interface FloatingUIComponentSignature { export default class FloatingUIComponent extends Component { @tracked _anchor: HTMLElement | null = null; + + @tracked content: HTMLElement | null = null; @tracked contentIsShown: boolean = false; @action registerAnchor(e: HTMLElement) { this._anchor = e; } + @action registerContent(e: HTMLElement) { + this.content = e; + } + get anchor() { assert("_anchor must exist", this._anchor); return this._anchor; diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 0ca0bea86..b79893987 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -7,6 +7,7 @@ @listIsOrdered={{true}} @onChange={{@onChange}} @selected={{@selected}} + class="max-h-[320px] w-80" > <:toggleButton as |f|>
    diff --git a/web/app/components/x/hds/dropdown/index.hbs b/web/app/components/x/hds/dropdown/index.hbs index 7806b16a1..82ac85446 100644 --- a/web/app/components/x/hds/dropdown/index.hbs +++ b/web/app/components/x/hds/dropdown/index.hbs @@ -1,40 +1,11 @@ - - - + <:anchor as |f|> + {{! default to a button if a toggleButton isn't used }} {{yield f to="toggleButton"}} @@ -42,15 +13,20 @@ <:content as |f|> 7; + } + get shownItems() { - console.log(this.args.items); return this.filteredItems || this.args.items; } @@ -50,6 +56,19 @@ export default class XHdsDropdownComponent extends Component< this._scrollContainer = element; } + @action protected didInsertList(f: any) { + schedule( + "afterRender", + () => { + assert("floatingUI content must exist", f.content); + this.assignMenuItemIDs( + f.content.querySelectorAll(`[role=${this.listItemRole}]`) + ); + }, + f + ); + } + @action willDestroyDropdown() { this.filteredItems = null; } @@ -176,30 +195,27 @@ export default class XHdsDropdownComponent extends Component< } protected onInput = restartableTask( - async (inputEvent: InputEvent, f: any) => { + async (f: any, inputEvent: InputEvent) => { + console.log(f); this.focusedItemIndex = -1; - // TODO: type the API interface + let showItems: any = {}; + let { items } = this.args; - // need some handling whether it's an object or an array - - // let shownFacets: FacetDropdownObjects = {}; - // let facets = this.args.facets; - - // this.query = (inputEvent.target as HTMLInputElement).value; - // for (const [key, value] of Object.entries(facets)) { - // if (key.toLowerCase().includes(this.query.toLowerCase())) { - // shownFacets[key] = value; - // } - // } + this.query = (inputEvent.target as HTMLInputElement).value; + for (const [key, value] of Object.entries(items)) { + if (key.toLowerCase().includes(this.query.toLowerCase())) { + showItems[key] = value; + } + } - // this.filteredItems = shownFacets; + this.filteredItems = showItems; - // schedule("afterRender", () => { - // this.assignMenuItemIDs( - // f.content.querySelectorAll(`[role=${this.listItemRole}]`) - // ); - // }); + schedule("afterRender", () => { + this.assignMenuItemIDs( + f.content.querySelectorAll(`[role=${this.listItemRole}]`) + ); + }); } ); } diff --git a/web/app/components/x/hds/dropdown/list-item.hbs b/web/app/components/x/hds/dropdown/list-item.hbs index 0591cf161..6fa8f885d 100644 --- a/web/app/components/x/hds/dropdown/list-item.hbs +++ b/web/app/components/x/hds/dropdown/list-item.hbs @@ -1,15 +1,32 @@ -
  • +
  • -
    +
    {{@value}}
    + {{#if @count}} + + {{/if}}
  • diff --git a/web/app/components/x/hds/dropdown/list-item.ts b/web/app/components/x/hds/dropdown/list-item.ts index 40e997329..efe53abc4 100644 --- a/web/app/components/x/hds/dropdown/list-item.ts +++ b/web/app/components/x/hds/dropdown/list-item.ts @@ -1,19 +1,111 @@ +import { assert } from "@ember/debug"; import { action } from "@ember/object"; +import RouterService from "@ember/routing/router-service"; +import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { FocusDirection } from "."; interface XHdsProductBadgeDropdownListSignature { Args: { role?: string; value?: any; selected: boolean; + focusedItemIndex: number; + listItemRole: string; hideDropdown: () => void; onChange: (value: any) => void; + setFocusedItemIndex: ( + focusDirection: FocusDirection | number, + maybeScrollIntoView?: boolean + ) => void; }; } export default class XHdsProductBadgeDropdownList extends Component { + @service declare router: RouterService; + /** + * The element reference, set on insertion and updated on mouseenter. + * Used to compute the element's ID, which may change when the list is filtered. + */ + @tracked private _domElement: HTMLElement | null = null; + + /** + * An asserted-true reference to the element. + */ + protected get domElement() { + assert("element must exist", this._domElement); + return this._domElement; + } + + /** + * The element's domID, e.g., "facet-dropdown-list-item-0" + * Which is computed by the parent component on render and when + * the FacetList is filtered. Parsed by `this.id` to get the + * numeric identifier for the element. + */ + private get domElementID() { + return this.domElement.id; + } + + /** + * The current route name, used to set the LinkTo's @route + */ + protected get currentRouteName(): string { + return this.router.currentRouteName; + } + + /** + * A numeric identifier for the element based on its id, + * as computed by the parent component on render and when + * the FacetList is filtered. Strips everything but the trailing number. + * Used to apply classes and aria-selected, and to direct the parent component's + * focus action toward the correct element. + * Regex reference: + * \d = Any digit 0-9 + * + = One or more of the preceding token + * $ = End of input + */ + protected get itemIndexNumber(): number { + return parseInt(this.domElementID.match(/\d+$/)?.[0] || "0", 10); + } + + protected get isAriaSelected(): boolean { + if (!this._domElement) { + // True when first computed, which happens + // before the element is inserted and registered. + return false; + } + if (this.args.focusedItemIndex === -1) { + return false; + } + return this.args.focusedItemIndex === this.itemIndexNumber; + } + + /** + * The action called on element insertion. Sets the local `element` + * reference to the domElement we know to be our target. + */ + @action protected registerElement(element: HTMLElement) { + this._domElement = element; + } + @action onClick() { this.args.onChange(this.args.value); + // if the click is on a link, this needs to happen in the next run loop so it doesn't interfere with embers LinkTo handling this.args.hideDropdown(); } + + /** + * Sets our local `element` reference to mouse target, + * to capture its ID, which may change when the list is filtered. + * Then, calls the parent component's `setFocusedItemIndex` action, + * directing focus to the current element. + */ + @action protected focusMouseTarget(e: MouseEvent) { + let target = e.target; + assert("target must be an element", target instanceof HTMLElement); + this._domElement = target; + this.args.setFocusedItemIndex(this.itemIndexNumber, false); + } } diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index 034c5d0ec..ee1be4668 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -1,8 +1,12 @@ -{{on-document "keydown" (fn this.maybeKeyboardNavigate @f)}} +{{on-document "keydown" this.maybeKeyboardNavigate}} -
    - Nice!! - {{#if this.inputIsShown}} +
    + {{#if @inputIsShown}}
    {{else}} {{#if @listIsOrdered}} -
      +
        {{#if @shownItems}} {{#each-in @shownItems as |product|}} {{/each-in}} {{/if}} diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index db6539c0e..29bf90d65 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -9,6 +9,7 @@ interface XHdsDropdownListSignature { items?: any; shownItems: any; selected: any; + inputIsShown?: boolean; isOrdered?: boolean; onChange: (e: Event) => void; resetFocusedItemIndex: () => void; @@ -23,17 +24,13 @@ export default class XHdsDropdownList extends Component< > { @tracked _input: HTMLInputElement | null = null; - get inputIsShown() { - return Object.keys(this.args.items).length > 7; - } - get input() { assert("input must exist", this._input); return this._input; } protected get noMatchesFound(): boolean { - if (!this.inputIsShown) { + if (!this.args.inputIsShown) { return false; } return Object.entries(this.args.shownItems).length === 0; From beb53e1c1696f5a86342dd10bb136e2523636478 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 12 Apr 2023 13:40:16 -0400 Subject: [PATCH 012/128] UX improvements, experiments --- web/app/components/inputs/product-select.hbs | 6 ++-- web/app/components/inputs/product-select.ts | 9 +++++- web/app/components/x/hds/dropdown/index.hbs | 32 ++++++++++++++------ web/app/components/x/hds/dropdown/index.ts | 8 ++--- web/app/components/x/hds/dropdown/list.hbs | 2 +- web/app/components/x/hds/dropdown/list.ts | 7 +++-- 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index b79893987..d7bcffc7e 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -5,7 +5,7 @@ @@ -13,8 +13,8 @@
        { @service("fetch") declare fetchSvc: FetchService; + @tracked selected = this.args.selected; + @tracked products: ProductAreas | undefined = undefined; @tracked shownProducts: ProductAreas | null = null; + @action onChange(newValue: any) { + this.selected = newValue; + this.args.onChange(newValue); + } + protected fetchProducts = task(async () => { try { let products = await this.fetchSvc diff --git a/web/app/components/x/hds/dropdown/index.hbs b/web/app/components/x/hds/dropdown/index.hbs index 82ac85446..50c90b9b6 100644 --- a/web/app/components/x/hds/dropdown/index.hbs +++ b/web/app/components/x/hds/dropdown/index.hbs @@ -1,15 +1,29 @@ <:anchor as |f|> - {{! default to a button if a toggleButton isn't used }} - - {{yield f to="toggleButton"}} - + {{#if (has-block "toggleButton")}} + {{! if we weren't to yield a button but a couple modifiers, would that work? }} + + + {{yield f to="toggleButton"}} + + {{else}} + + {{/if}} <:content as |f|> { + next(() => { switch (event.key) { case "ArrowDown": this.setFocusedItemIndex(FocusDirection.First, false); diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index ee1be4668..f8054e686 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -25,7 +25,7 @@ />
        {{/if}} -
        +
        {{#if this.noMatchesFound}}
        Date: Wed, 12 Apr 2023 14:55:40 -0400 Subject: [PATCH 013/128] WIP - IDs --- web/app/components/floating-u-i/index.ts | 3 + web/app/components/x/hds/dropdown/index.hbs | 11 ++-- .../components/x/hds/dropdown/list-items.hbs | 13 +++++ web/app/components/x/hds/dropdown/list.hbs | 57 ++++++++++++------- web/app/components/x/hds/dropdown/list.ts | 16 ++++++ web/app/templates/playground.hbs | 17 ++---- 6 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 web/app/components/x/hds/dropdown/list-items.hbs diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 01daaa8e6..7ba750443 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -1,5 +1,6 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; +import { guidFor } from "@ember/object/internals"; import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; @@ -25,6 +26,8 @@ export default class FloatingUIComponent extends Component {{yield f to="toggleButton"}} @@ -26,12 +26,15 @@ {{/if}} <:content as |f|> + {{! action list }} + {{! link list }} + {{/each-in}} +{{/if}} diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index f8054e686..fd9dc707c 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -20,15 +20,18 @@ aria-controls="x-hds-dropdown" aria-activedescendant={{unless (eq @focusedItemIndex -1) - (concat "facet-dropdown-menu-item-" @focusedItemIndex) + (concat "x-hds-dropdown-list-item-" @focusedItemIndex) }} />
        {{/if}} -
        +
        {{#if this.noMatchesFound}}
        No matches @@ -36,28 +39,38 @@ {{else}} {{#if @listIsOrdered}}
          - {{#if @shownItems}} - {{#each-in @shownItems as |product|}} - - {{/each-in}} - {{/if}} +
        + {{else}} +
          + +
        {{/if}} {{/if}}
        diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index 2a7cf39b8..b661e0029 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -6,9 +6,11 @@ import { FocusDirection } from "."; interface XHdsDropdownListSignature { Args: { + id: string; items?: any; shownItems: any; selected: any; + focusedItemIndex: number; inputIsShown?: boolean; isOrdered?: boolean; onChange: (e: Event) => void; @@ -29,6 +31,20 @@ export default class XHdsDropdownList extends Component< return this._input; } + get id(): string { + return `x-hds-dropdown-list-${this.id}`; + } + + get role() { + return this.args.inputIsShown ? "listbox" : "menu"; + } + + get ariaActiveDescendant() { + if (this.args.focusedItemIndex !== -1) { + return `x-hds-dropdown-list-item-${this.args.focusedItemIndex}`; + } + } + protected get noMatchesFound(): boolean { if (!this.args.inputIsShown) { return false; diff --git a/web/app/templates/playground.hbs b/web/app/templates/playground.hbs index 9ff46fdec..a1020ea1b 100644 --- a/web/app/templates/playground.hbs +++ b/web/app/templates/playground.hbs @@ -1,13 +1,6 @@ Playground - - <:anchor as |f|> - - Toggle Content - - - <:content as |f|> -
        - This is the content -
        - -
        + + <:toggleButton> + What did I do now + + From 2e236d1b25a53474133c1c742ab4be135ff9cf80 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 12 Apr 2023 18:13:38 -0400 Subject: [PATCH 014/128] Minor refactor and cleanup --- web/app/components/document/sidebar.hbs | 2 +- web/app/components/inputs/product-select.hbs | 23 +++++++++++---- web/app/components/inputs/product-select.ts | 1 - web/app/components/x/hds/dropdown/index.hbs | 28 +++---------------- web/app/components/x/hds/dropdown/index.ts | 16 +++++++---- .../components/x/hds/dropdown/list-items.hbs | 2 +- web/app/components/x/hds/dropdown/list.ts | 2 +- 7 files changed, 36 insertions(+), 38 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index a4ef2b836..0d2cfaf07 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -136,7 +136,7 @@ {{#if this.isDraft}} {{else}}
        +
        {{/unless}} +{{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} + - <:toggleButton as |f|> -
        + <:anchor as |d|> + -
        - + +
        diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 02dc5a939..428f4a83b 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -25,7 +25,6 @@ export default class InputsProductSelectSignature extends Component <:anchor as |f|> - {{#if (has-block "toggleButton")}} - {{! if we weren't to yield a button but a couple modifiers, would that work? }} - - - {{yield f to="toggleButton"}} - - {{else}} - - {{/if}} + {{yield + (hash onTriggerKeydown=this.onTriggerKeydown floating=f) + to="anchor" + }} <:content as |f|> {{! action list }} diff --git a/web/app/components/x/hds/dropdown/index.ts b/web/app/components/x/hds/dropdown/index.ts index bf48ce539..25d81579c 100644 --- a/web/app/components/x/hds/dropdown/index.ts +++ b/web/app/components/x/hds/dropdown/index.ts @@ -52,6 +52,17 @@ export default class XHdsDropdownComponent extends Component< return this.filteredItems || this.args.items; } + get ariaControls() { + let value = "x-hds-dropdown-"; + if (this.inputIsShown) { + value += "popover"; + } else { + value += "list"; + } + + return `${value}-`; + } + @action protected registerScrollContainer(element: HTMLDivElement) { this._scrollContainer = element; } @@ -73,11 +84,6 @@ export default class XHdsDropdownComponent extends Component< this.filteredItems = null; } - @action onSelect(product: string, hideDropdown: () => void) { - this.args.onChange(product); - hideDropdown(); - } - @action assignMenuItemIDs(items: NodeListOf): void { this.menuItems = items; for (let i = 0; i < items.length; i++) { diff --git a/web/app/components/x/hds/dropdown/list-items.hbs b/web/app/components/x/hds/dropdown/list-items.hbs index 6ebe5a109..47f00e294 100644 --- a/web/app/components/x/hds/dropdown/list-items.hbs +++ b/web/app/components/x/hds/dropdown/list-items.hbs @@ -1,7 +1,7 @@ {{#if @shownItems}} {{#each-in @shownItems as |item|}} Date: Thu, 13 Apr 2023 09:30:01 -0400 Subject: [PATCH 015/128] Update list.hbs --- web/app/components/x/hds/dropdown/list.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index fd9dc707c..708afc52d 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -47,6 +47,7 @@ > Date: Thu, 13 Apr 2023 09:52:37 -0400 Subject: [PATCH 016/128] Reorganize FloatingUI components --- web/app/components/floating-u-i/content.hbs | 4 ++-- web/app/components/floating-u-i/content.ts | 20 ++------------------ web/app/components/floating-u-i/index.ts | 15 +++++++-------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index ccc9d8b9a..af16b8be6 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -1,13 +1,13 @@ {{#maybe-in-element (html-element ".ember-application") - this.shouldRenderInPlace + (not @renderOut) insertBefore=null }}
        diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 1562e1376..f20fdb9db 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -12,7 +12,6 @@ import { } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; -import htmlElement from "hermes/utils/html-element"; interface FloatingUIContentSignature { Args: { @@ -24,14 +23,7 @@ interface FloatingUIContentSignature { export default class FloatingUIContent extends Component { @tracked _content: HTMLElement | null = null; - - get shouldRenderInPlace() { - if (this.args.renderOut) { - return false; - } else { - return true; - } - } + @tracked cleanup: (() => void) | null = null; get id() { return guidFor(this); @@ -42,12 +34,6 @@ export default class FloatingUIContent extends Component void) | null = null; - @action didInsert(e: HTMLElement) { this._content = e; @@ -57,7 +43,7 @@ export default class FloatingUIContent extends Component { - this.content.setAttribute("data-popover-placement", placement); + this.content.setAttribute("data-floating-ui-placement", placement); Object.assign(this.content.style, { left: `${x}px`, @@ -66,8 +52,6 @@ export default class FloatingUIContent extends Component { - @tracked _anchor: HTMLElement | null = null; + readonly id = guidFor(this); + @tracked _anchor: HTMLElement | null = null; @tracked content: HTMLElement | null = null; @tracked contentIsShown: boolean = false; + get anchor() { + assert("_anchor must exist", this._anchor); + return this._anchor; + } + @action registerAnchor(e: HTMLElement) { this._anchor = e; } @@ -26,13 +32,6 @@ export default class FloatingUIComponent extends Component Date: Thu, 13 Apr 2023 09:54:57 -0400 Subject: [PATCH 017/128] Update content.ts --- web/app/components/floating-u-i/content.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index f20fdb9db..61a29c0c8 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -22,13 +22,11 @@ interface FloatingUIContentSignature { } export default class FloatingUIContent extends Component { + readonly id = guidFor(this); + @tracked _content: HTMLElement | null = null; @tracked cleanup: (() => void) | null = null; - get id() { - return guidFor(this); - } - get content() { assert("_content must exist", this._content); return this._content; From 4be0635443c367b4d504d327a6cb30b8e51593d6 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 17 Apr 2023 10:35:52 -0400 Subject: [PATCH 018/128] Improve loading strategy --- web/app/components/inputs/product-select.hbs | 77 +++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 7840ed165..214b958ee 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,38 +1,43 @@ -{{#unless this.products}} -
        -{{/unless}} - {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} +{{#if this.products}} + + <:anchor as |d|> + + + + + + +{{else if this.fetchProducts.isRunning}} + +{{else}} +
        - - <:anchor as |d|> - - - - - - +{{/if}} From 34603f445ed536502ded9b07bab5a1ed32d35967 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 12:49:37 -0400 Subject: [PATCH 019/128] Add `ember-element-helper` --- web/package.json | 1 + web/yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 963d066f0..c1636d71f 100644 --- a/web/package.json +++ b/web/package.json @@ -77,6 +77,7 @@ "ember-click-outside-modifier": "^4.0.0", "ember-composable-helpers": "^5.0.0", "ember-data": "~3.28.6", + "ember-element-helper": "^0.6.1", "ember-export-application-global": "^2.0.1", "ember-fetch": "^8.1.1", "ember-focus-trap": "^1.0.1", diff --git a/web/yarn.lock b/web/yarn.lock index 44de1611e..2871f152a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -8357,7 +8357,7 @@ __metadata: languageName: node linkType: hard -"ember-element-helper@npm:^0.6.0": +"ember-element-helper@npm:^0.6.0, ember-element-helper@npm:^0.6.1": version: 0.6.1 resolution: "ember-element-helper@npm:0.6.1" dependencies: @@ -10983,6 +10983,7 @@ __metadata: ember-composable-helpers: ^5.0.0 ember-concurrency: ^2.2.1 ember-data: ~3.28.6 + ember-element-helper: ^0.6.1 ember-export-application-global: ^2.0.1 ember-fetch: ^8.1.1 ember-focus-trap: ^1.0.1 From 9a9dc7edc25201fd9efbb2869434f0e0202a503d Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 12:54:25 -0400 Subject: [PATCH 020/128] Refactor dropdown list container --- web/app/components/x/hds/dropdown/list.hbs | 27 ++++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index 708afc52d..38b349d36 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -37,9 +37,8 @@ No matches
        {{else}} - {{#if @listIsOrdered}} -
          -
        - {{else}} -
          - -
        - {{/if}} + + {{/let}} {{/if}}
        From b3b47a18085afff1c8953e78382b3caa039ee986 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 15:18:12 -0400 Subject: [PATCH 021/128] Template tweaks --- .../components/x/hds/dropdown/list-items.hbs | 13 ------- web/app/components/x/hds/dropdown/list.hbs | 39 ++++++++++++------- web/app/components/x/hds/dropdown/list.ts | 1 + 3 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 web/app/components/x/hds/dropdown/list-items.hbs diff --git a/web/app/components/x/hds/dropdown/list-items.hbs b/web/app/components/x/hds/dropdown/list-items.hbs deleted file mode 100644 index 47f00e294..000000000 --- a/web/app/components/x/hds/dropdown/list-items.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#if @shownItems}} - {{#each-in @shownItems as |item|}} - - {{/each-in}} -{{/if}} diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index 38b349d36..b2e259c2f 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -1,12 +1,14 @@ +{{! Handle ArrowUp, ArrowDown and Enter: }} {{on-document "keydown" this.maybeKeyboardNavigate}}
        {{#if @inputIsShown}} + {{! TODO: make this a class }}
        {{/if}}
        {{#if this.noMatchesFound}}
        + {{! Need to template-ize this }} No matches
        {{else}} - {{#let (element (if @listIsOrdered "ol" "ul")) as |List|}} - - - + {{#if @shownItems}} + {{#each-in @shownItems as |item|}} + + {{/each-in}} + {{/if}} + {{/let}} {{/if}}
        diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index 57b5b4e80..af89e6326 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -12,6 +12,7 @@ interface XHdsDropdownListSignature { selected: any; focusedItemIndex: number; inputIsShown?: boolean; + inputPlaceholder?: string; isOrdered?: boolean; onChange: (e: Event) => void; resetFocusedItemIndex: () => void; From 788c5db4283c797a2dae63bcebfced61431d208b Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 15:27:43 -0400 Subject: [PATCH 022/128] Template and CSS cleanup --- web/app/components/x/hds/dropdown/list.hbs | 20 ++++++++----------- web/app/components/x/hds/dropdown/list.ts | 8 -------- web/app/styles/app.scss | 1 + .../components/x/hds/dropdown/list.scss | 19 ++++++++++++++++++ 4 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 web/app/styles/components/x/hds/dropdown/list.scss diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index b2e259c2f..f534099f9 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -2,14 +2,13 @@ {{on-document "keydown" this.maybeKeyboardNavigate}}
        {{#if @inputIsShown}} - {{! TODO: make this a class }} -
        +
        {{/if}}
        {{#if this.noMatchesFound}}
        - {{! Need to template-ize this }} No matches
        {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} {{#if @shownItems}} diff --git a/web/app/components/x/hds/dropdown/list.ts b/web/app/components/x/hds/dropdown/list.ts index af89e6326..a7199c2b7 100644 --- a/web/app/components/x/hds/dropdown/list.ts +++ b/web/app/components/x/hds/dropdown/list.ts @@ -32,14 +32,6 @@ export default class XHdsDropdownList extends Component< return this._input; } - get id(): string { - return `x-hds-dropdown-list-${this.args.id}`; - } - - get role() { - return this.args.inputIsShown ? "listbox" : "menu"; - } - get ariaActiveDescendant() { if (this.args.focusedItemIndex !== -1) { return `x-hds-dropdown-list-item-${this.args.focusedItemIndex}`; diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index fc9c84442..a937a65c6 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -5,6 +5,7 @@ @use "components/footer"; @use "components/nav"; @use "components/x-hds-tab"; +@use "components/x/hds/dropdown/list"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; diff --git a/web/app/styles/components/x/hds/dropdown/list.scss b/web/app/styles/components/x/hds/dropdown/list.scss new file mode 100644 index 000000000..7e6206d86 --- /dev/null +++ b/web/app/styles/components/x/hds/dropdown/list.scss @@ -0,0 +1,19 @@ +.x-hds-dropdown-list-container { + @apply flex flex-col overflow-hidden; +} + +.x-hds-dropdown-list-input-container { + @apply relative p-1 border-b border-b-color-border-faint; +} + +.x-hds-dropdown-list-scroll-container { + @apply overflow-auto w-full relative; +} + +.x-hds-dropdown-list-empty-state { + @apply p-12 text-center text-color-foreground-faint; +} + +.x-hds-dropdown-list { + @apply py-1; +} From d7c0bbd03084be5f57ee1a5774608c17bc600de2 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 16:39:17 -0400 Subject: [PATCH 023/128] CSS cleanup --- .../components/x/hds/dropdown/list-item.hbs | 12 +++---- web/app/components/x/hds/dropdown/list.hbs | 5 +-- web/app/styles/app.scss | 1 + .../components/x/hds/dropdown/list-item.scss | 35 +++++++++++++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 web/app/styles/components/x/hds/dropdown/list-item.scss diff --git a/web/app/components/x/hds/dropdown/list-item.hbs b/web/app/components/x/hds/dropdown/list-item.hbs index 6fa8f885d..590fa2108 100644 --- a/web/app/components/x/hds/dropdown/list-item.hbs +++ b/web/app/components/x/hds/dropdown/list-item.hbs @@ -1,4 +1,4 @@ -
      1. +
      2. -
        +
        {{@value}}
        {{#if @count}} {{/if}} diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown/list.hbs index f534099f9..c27d15f59 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown/list.hbs @@ -33,10 +33,7 @@ class="x-hds-dropdown-list-scroll-container" > {{#if this.noMatchesFound}} -
        +
        No matches
        {{else}} diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index a937a65c6..aba3d705d 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -6,6 +6,7 @@ @use "components/nav"; @use "components/x-hds-tab"; @use "components/x/hds/dropdown/list"; +@use "components/x/hds/dropdown/list-item"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; diff --git a/web/app/styles/components/x/hds/dropdown/list-item.scss b/web/app/styles/components/x/hds/dropdown/list-item.scss new file mode 100644 index 000000000..2c5c95ad9 --- /dev/null +++ b/web/app/styles/components/x/hds/dropdown/list-item.scss @@ -0,0 +1,35 @@ +.x-hds-dropdown-list-item { + @apply flex; +} + +.x-hds-dropdown-list-item-link { + @apply no-underline flex items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; + + &.is-aria-selected { + @apply bg-color-foreground-action text-color-foreground-high-contrast outline-none; + + .flight-icon { + @apply text-inherit; + } + } + + .flight-icon { + @apply text-color-foreground-action shrink-0; + + &.check { + @apply mr-2.5; + } + + &.sort-icon { + @apply ml-3 mr-4; + } + } +} + +.x-hds-dropdown-list-item-value { + @apply truncate whitespace-nowrap; +} + +.x-hds-dropdown-list-item-count { + @apply ml-8 shrink-0; +} From 846b445999791cbb2c5ecc268f69a9e0a7c7e284 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 16:43:29 -0400 Subject: [PATCH 024/128] API tweak --- web/app/components/inputs/product-select.hbs | 8 ++++---- web/app/components/x/hds/dropdown/index.hbs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 214b958ee..c096adbd1 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -10,14 +10,14 @@ <:anchor as |d|> <:anchor as |f|> {{yield - (hash onTriggerKeydown=this.onTriggerKeydown floating=f) + (hash onTriggerKeydown=this.onTriggerKeydown floatingUI=f) to="anchor" }} From e834cddb20f2e4a4a44072ec003dd532d61f2381 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 17:06:06 -0400 Subject: [PATCH 025/128] Rename files --- web/app/components/header/facet-dropdown-list-item.ts | 2 +- web/app/components/header/facet-dropdown-list.ts | 2 +- web/app/components/header/facet-dropdown.ts | 2 +- web/app/components/inputs/product-select.hbs | 6 +++--- .../components/x/hds/{dropdown => dropdown-list}/index.hbs | 2 +- .../components/x/hds/{dropdown => dropdown-list}/index.ts | 6 +++--- .../hds/{dropdown/list-item.hbs => dropdown-list/item.hbs} | 0 .../x/hds/{dropdown/list-item.ts => dropdown-list/item.ts} | 4 ++-- .../x/hds/{dropdown/list.hbs => dropdown-list/items.hbs} | 2 +- .../x/hds/{dropdown/list.ts => dropdown-list/items.ts} | 6 +++--- web/app/templates/playground.hbs | 6 ------ .../components/header/facet-dropdown-list-test.ts | 2 +- 12 files changed, 17 insertions(+), 23 deletions(-) rename web/app/components/x/hds/{dropdown => dropdown-list}/index.hbs (96%) rename web/app/components/x/hds/{dropdown => dropdown-list}/index.ts (97%) rename web/app/components/x/hds/{dropdown/list-item.hbs => dropdown-list/item.hbs} (100%) rename web/app/components/x/hds/{dropdown/list-item.ts => dropdown-list/item.ts} (95%) rename web/app/components/x/hds/{dropdown/list.hbs => dropdown-list/items.hbs} (97%) rename web/app/components/x/hds/{dropdown/list.ts => dropdown-list/items.ts} (91%) diff --git a/web/app/components/header/facet-dropdown-list-item.ts b/web/app/components/header/facet-dropdown-list-item.ts index 640c8d669..3f0918d44 100644 --- a/web/app/components/header/facet-dropdown-list-item.ts +++ b/web/app/components/header/facet-dropdown-list-item.ts @@ -6,7 +6,7 @@ import { tracked } from "@glimmer/tracking"; import { assert } from "@ember/debug"; import { next } from "@ember/runloop"; import { SortByLabel, SortByValue } from "./toolbar"; -import { FocusDirection } from "../x/hds/dropdown"; +import { FocusDirection } from "../x/hds/dropdown-list"; enum FacetDropdownAriaRole { Option = "option", diff --git a/web/app/components/header/facet-dropdown-list.ts b/web/app/components/header/facet-dropdown-list.ts index 271299efa..bcfcab801 100644 --- a/web/app/components/header/facet-dropdown-list.ts +++ b/web/app/components/header/facet-dropdown-list.ts @@ -5,7 +5,7 @@ import { tracked } from "@glimmer/tracking"; import { assert } from "@ember/debug"; import { inject as service } from "@ember/service"; import RouterService from "@ember/routing/router-service"; -import { FocusDirection } from "../x/hds/dropdown"; +import { FocusDirection } from "../x/hds/dropdown-list"; interface HeaderFacetDropdownListComponentSignature { Args: { diff --git a/web/app/components/header/facet-dropdown.ts b/web/app/components/header/facet-dropdown.ts index a6207c240..4033c2636 100644 --- a/web/app/components/header/facet-dropdown.ts +++ b/web/app/components/header/facet-dropdown.ts @@ -5,7 +5,7 @@ import { tracked } from "@glimmer/tracking"; import { assert } from "@ember/debug"; import { restartableTask } from "ember-concurrency"; import { schedule } from "@ember/runloop"; -import { FocusDirection } from "../x/hds/dropdown"; +import { FocusDirection } from "../x/hds/dropdown-list"; interface FacetDropdownComponentSignature { Args: { diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index c096adbd1..3a4744790 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,6 +1,7 @@ {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} - - + {{else if this.fetchProducts.isRunning}} {{else}} @@ -39,5 +40,4 @@ class="absolute top-0 left-0" {{did-insert (perform this.fetchProducts)}} >
        - {{/if}} diff --git a/web/app/components/x/hds/dropdown/index.hbs b/web/app/components/x/hds/dropdown-list/index.hbs similarity index 96% rename from web/app/components/x/hds/dropdown/index.hbs rename to web/app/components/x/hds/dropdown-list/index.hbs index f444edda1..e8a7a2193 100644 --- a/web/app/components/x/hds/dropdown/index.hbs +++ b/web/app/components/x/hds/dropdown-list/index.hbs @@ -8,7 +8,7 @@ <:content as |f|> {{! action list }} {{! link list }} - { +interface XHdsDropdownListComponentSignature { Args: { selected: any; items?: any; @@ -23,8 +23,8 @@ export enum FocusDirection { Last = "last", } -export default class XHdsDropdownComponent extends Component< - XHdsDropdownComponentSignature +export default class XHdsDropdownListComponent extends Component< + XHdsDropdownListComponentSignature > { @service("fetch") declare fetchSvc: FetchService; diff --git a/web/app/components/x/hds/dropdown/list-item.hbs b/web/app/components/x/hds/dropdown-list/item.hbs similarity index 100% rename from web/app/components/x/hds/dropdown/list-item.hbs rename to web/app/components/x/hds/dropdown-list/item.hbs diff --git a/web/app/components/x/hds/dropdown/list-item.ts b/web/app/components/x/hds/dropdown-list/item.ts similarity index 95% rename from web/app/components/x/hds/dropdown/list-item.ts rename to web/app/components/x/hds/dropdown-list/item.ts index efe53abc4..1e71754b0 100644 --- a/web/app/components/x/hds/dropdown/list-item.ts +++ b/web/app/components/x/hds/dropdown-list/item.ts @@ -6,7 +6,7 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; -interface XHdsProductBadgeDropdownListSignature { +interface XHdsDropdownListItemComponentSignature { Args: { role?: string; value?: any; @@ -22,7 +22,7 @@ interface XHdsProductBadgeDropdownListSignature { }; } -export default class XHdsProductBadgeDropdownList extends Component { +export default class XHdsDropdownListItemComponent extends Component { @service declare router: RouterService; /** * The element reference, set on insertion and updated on mouseenter. diff --git a/web/app/components/x/hds/dropdown/list.hbs b/web/app/components/x/hds/dropdown-list/items.hbs similarity index 97% rename from web/app/components/x/hds/dropdown/list.hbs rename to web/app/components/x/hds/dropdown-list/items.hbs index c27d15f59..25867a6d6 100644 --- a/web/app/components/x/hds/dropdown/list.hbs +++ b/web/app/components/x/hds/dropdown-list/items.hbs @@ -46,7 +46,7 @@ > {{#if @shownItems}} {{#each-in @shownItems as |item|}} - { +interface XHdsDropdownListItemsComponentSignature { Args: { id: string; items?: any; @@ -22,8 +22,8 @@ interface XHdsDropdownListSignature { }; } -export default class XHdsDropdownList extends Component< - XHdsDropdownListSignature +export default class XHdsDropdownListItemsComponent extends Component< + XHdsDropdownListItemsComponentSignature > { @tracked _input: HTMLInputElement | null = null; diff --git a/web/app/templates/playground.hbs b/web/app/templates/playground.hbs index a1020ea1b..e69de29bb 100644 --- a/web/app/templates/playground.hbs +++ b/web/app/templates/playground.hbs @@ -1,6 +0,0 @@ -Playground - - <:toggleButton> - What did I do now - - diff --git a/web/tests/integration/components/header/facet-dropdown-list-test.ts b/web/tests/integration/components/header/facet-dropdown-list-test.ts index 7b0b1962a..35e2f6c3a 100644 --- a/web/tests/integration/components/header/facet-dropdown-list-test.ts +++ b/web/tests/integration/components/header/facet-dropdown-list-test.ts @@ -4,7 +4,7 @@ import { find, findAll, render, triggerKeyEvent } from "@ember/test-helpers"; import { assert as emberAssert } from "@ember/debug"; import { hbs } from "ember-cli-htmlbars"; import { LONG_FACET_LIST, SHORT_FACET_LIST } from "./facet-dropdown-test"; -import { FocusDirection } from "hermes/components/x/hds/dropdown"; +import { FocusDirection } from "hermes/components/x/hds/dropdown-list"; module( "Integration | Component | header/facet-dropdown-list", From f80e982f1ffa028317a5b38eb9b0a54a0c350ed4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 19 Apr 2023 17:29:24 -0400 Subject: [PATCH 026/128] Rearrange files --- web/app/components/inputs/product-select.hbs | 4 +- .../components/x/hds/dropdown-list/index.hbs | 72 ++++++++++----- .../components/x/hds/dropdown-list/index.ts | 12 +++ .../components/x/hds/dropdown-list/items.hbs | 87 ++++++------------- .../components/x/hds/dropdown-list/items.ts | 13 --- 5 files changed, 92 insertions(+), 96 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 3a4744790..baf0ff022 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,6 +1,5 @@ {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} - yut <:anchor as |d|> + ...attributes + > + {{#if this.inputIsShown}} +
        + +
        + {{/if}} +
        + + +
        + {{! action list }} + {{! link list }} +
        diff --git a/web/app/components/x/hds/dropdown-list/index.ts b/web/app/components/x/hds/dropdown-list/index.ts index c99cdb181..7a77afc5e 100644 --- a/web/app/components/x/hds/dropdown-list/index.ts +++ b/web/app/components/x/hds/dropdown-list/index.ts @@ -39,11 +39,18 @@ export default class XHdsDropdownListComponent extends Component< @tracked filteredItems: unknown | null = null; @tracked protected menuItems: NodeListOf | null = null; + @tracked _input: HTMLInputElement | null = null; + private get scrollContainer(): HTMLElement { assert("_scrollContainer must exist", this._scrollContainer); return this._scrollContainer; } + get input() { + assert("input must exist", this._input); + return this._input; + } + get inputIsShown() { return Object.keys(this.args.items).length > 7; } @@ -67,6 +74,11 @@ export default class XHdsDropdownListComponent extends Component< this._scrollContainer = element; } + @action registerAndFocusInput(e: HTMLInputElement) { + this._input = e; + this.input.focus(); + } + @action protected didInsertList(f: any) { schedule( "afterRender", diff --git a/web/app/components/x/hds/dropdown-list/items.hbs b/web/app/components/x/hds/dropdown-list/items.hbs index 25867a6d6..f1acd2037 100644 --- a/web/app/components/x/hds/dropdown-list/items.hbs +++ b/web/app/components/x/hds/dropdown-list/items.hbs @@ -1,64 +1,31 @@ {{! Handle ArrowUp, ArrowDown and Enter: }} {{on-document "keydown" this.maybeKeyboardNavigate}} -
        - {{#if @inputIsShown}} -
        - -
        - {{/if}} -
        - {{#if this.noMatchesFound}} -
        - No matches -
        - {{else}} - {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} - - {{#if @shownItems}} - {{#each-in @shownItems as |item|}} - - {{/each-in}} - {{/if}} - - {{/let}} - {{/if}} +{{#if this.noMatchesFound}} +
        + No matches
        -
        +{{else}} + {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} + + {{#if @shownItems}} + {{#each-in @shownItems as |item|}} + + {{/each-in}} + {{/if}} + + {{/let}} +{{/if}} diff --git a/web/app/components/x/hds/dropdown-list/items.ts b/web/app/components/x/hds/dropdown-list/items.ts index 71219e511..55441b48b 100644 --- a/web/app/components/x/hds/dropdown-list/items.ts +++ b/web/app/components/x/hds/dropdown-list/items.ts @@ -1,7 +1,6 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; interface XHdsDropdownListItemsComponentSignature { @@ -25,13 +24,6 @@ interface XHdsDropdownListItemsComponentSignature { export default class XHdsDropdownListItemsComponent extends Component< XHdsDropdownListItemsComponentSignature > { - @tracked _input: HTMLInputElement | null = null; - - get input() { - assert("input must exist", this._input); - return this._input; - } - get ariaActiveDescendant() { if (this.args.focusedItemIndex !== -1) { return `x-hds-dropdown-list-item-${this.args.focusedItemIndex}`; @@ -45,11 +37,6 @@ export default class XHdsDropdownListItemsComponent extends Component< return Object.entries(this.args.shownItems).length === 0; } - @action registerAndFocusInput(e: HTMLInputElement) { - this._input = e; - this.input.focus(); - } - @action protected maybeKeyboardNavigate(event: KeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); From 3ab153e0d3edf933f0446b40af977405df5acfb0 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 20 Apr 2023 09:29:05 -0400 Subject: [PATCH 027/128] Update index.hbs --- web/app/components/x/hds/dropdown-list/index.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/app/components/x/hds/dropdown-list/index.hbs b/web/app/components/x/hds/dropdown-list/index.hbs index 5ce6e1257..fc0ddb4aa 100644 --- a/web/app/components/x/hds/dropdown-list/index.hbs +++ b/web/app/components/x/hds/dropdown-list/index.hbs @@ -38,6 +38,8 @@ {{did-insert this.registerScrollContainer}} class="x-hds-dropdown-list-scroll-container" > + {{! So the problem is we want the flexibility to use LinkTos or Actions, + each perhaps with its own unique requirements. Need to find some way of yielding the list item with a quick way of tagging the interactive component with stuff }} Date: Thu, 20 Apr 2023 10:47:54 -0400 Subject: [PATCH 028/128] Yield items to main component --- web/app/components/inputs/product-select.hbs | 24 ++++++++- .../x/hds/dropdown-list/checkable-item | 11 +++++ .../x/hds/dropdown-list/checkable-item.hbs | 14 ++++++ .../components/x/hds/dropdown-list/index.hbs | 8 ++- .../components/x/hds/dropdown-list/item.hbs | 27 +--------- .../components/x/hds/dropdown-list/item.ts | 49 +++++++++++++------ .../components/x/hds/dropdown-list/items.hbs | 13 +++-- 7 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 web/app/components/x/hds/dropdown-list/checkable-item create mode 100644 web/app/components/x/hds/dropdown-list/checkable-item.hbs diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index baf0ff022..ac9836cfc 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -3,7 +3,7 @@ @@ -12,9 +12,10 @@ {{did-insert d.floatingUI.registerAnchor}} {{on "keydown" (fn d.onTriggerKeydown d.floatingUI)}} {{on "click" d.floatingUI.toggleContent}} + aria-haspopup="listbox" aria-controls="x-hds-dropdown-list-{{if d.inputIsShown - 'popover' + 'container' 'list' }}-{{d.floatingUI.id}}" class="relative flex" @@ -30,6 +31,25 @@ /> + <:item as |i|> + + + + {{else if this.fetchProducts.isRunning}} diff --git a/web/app/components/x/hds/dropdown-list/checkable-item b/web/app/components/x/hds/dropdown-list/checkable-item new file mode 100644 index 000000000..5df85f85e --- /dev/null +++ b/web/app/components/x/hds/dropdown-list/checkable-item @@ -0,0 +1,11 @@ +import Component from "@glimmer/component"; + +interface XHdsDropdownListCheckableItemComponentSignature { + Args: { + selected: boolean; + value: string; + count?: number; + }; +} + +export default class XHdsDropdownListCheckableItemComponent extends Component {} diff --git a/web/app/components/x/hds/dropdown-list/checkable-item.hbs b/web/app/components/x/hds/dropdown-list/checkable-item.hbs new file mode 100644 index 000000000..0cc22767a --- /dev/null +++ b/web/app/components/x/hds/dropdown-list/checkable-item.hbs @@ -0,0 +1,14 @@ + +
        + {{@value}} +
        +{{#if @count}} + +{{/if}} diff --git a/web/app/components/x/hds/dropdown-list/index.hbs b/web/app/components/x/hds/dropdown-list/index.hbs index fc0ddb4aa..a2b9f81a0 100644 --- a/web/app/components/x/hds/dropdown-list/index.hbs +++ b/web/app/components/x/hds/dropdown-list/index.hbs @@ -44,7 +44,7 @@ @items={{@items}} @id={{f.id}} @onInput={{perform this.onInput f}} - @onChange={{@onChange}} + @onItemClick={{@onItemClick}} @listItemRole={{this.listItemRole}} @resetFocusedItemIndex={{this.resetFocusedItemIndex}} @registerScrollContainer={{this.registerScrollContainer}} @@ -56,7 +56,11 @@ @selected={{@selected}} @query={{this.query}} @f={{f}} - /> + > + <:item as |i|> + {{yield i to="item"}} + +
        {{! action list }} diff --git a/web/app/components/x/hds/dropdown-list/item.hbs b/web/app/components/x/hds/dropdown-list/item.hbs index 590fa2108..e358b34f2 100644 --- a/web/app/components/x/hds/dropdown-list/item.hbs +++ b/web/app/components/x/hds/dropdown-list/item.hbs @@ -1,28 +1,3 @@
      3. - - -
        - {{@value}} -
        - {{#if @count}} - - {{/if}} -
        + {{yield this}}
      4. diff --git a/web/app/components/x/hds/dropdown-list/item.ts b/web/app/components/x/hds/dropdown-list/item.ts index 1e71754b0..b4fd3afe1 100644 --- a/web/app/components/x/hds/dropdown-list/item.ts +++ b/web/app/components/x/hds/dropdown-list/item.ts @@ -5,16 +5,18 @@ import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; +import { next } from "@ember/runloop"; interface XHdsDropdownListItemComponentSignature { Args: { role?: string; value?: any; + count?: number; selected: boolean; focusedItemIndex: number; listItemRole: string; hideDropdown: () => void; - onChange: (value: any) => void; + onItemClick?: (value: any) => void; setFocusedItemIndex: ( focusDirection: FocusDirection | number, maybeScrollIntoView?: boolean @@ -30,6 +32,22 @@ export default class XHdsDropdownListItemComponent extends Component handling. + */ + next(() => { + this.args.hideDropdown(); + }); } /** @@ -102,7 +121,7 @@ export default class XHdsDropdownListItemComponent extends Component No matches
        + {{/if}} {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} + as |i| + > + {{yield i to="item"}} + {{/each-in}} {{/if}} From 01480875f53ea76f11242ff9744f9a0908b4f78c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 20 Apr 2023 11:40:24 -0400 Subject: [PATCH 029/128] Port facetDropdown to new Listbox --- web/app/components/header/facet-dropdown.hbs | 114 +++++++++++------- web/app/components/header/facet-dropdown.ts | 8 ++ web/app/components/inputs/product-select.hbs | 1 + .../components/x/hds/dropdown-list/item.ts | 11 +- .../components/x/hds/dropdown-list/items.hbs | 3 +- .../components/x/hds/dropdown-list/items.ts | 2 + 6 files changed, 92 insertions(+), 47 deletions(-) diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 78b7cc7ae..63c640dff 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -6,48 +6,76 @@ https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions-active-descendant/ }} -
        - - {{#if this.dropdownIsShown}} - + - {{/if}} -
        + + <:item as |i|> + {{log i}} + + + + + +{{!-- +{{#if this.dropdownIsShown}} + +{{/if}} --}} diff --git a/web/app/components/header/facet-dropdown.ts b/web/app/components/header/facet-dropdown.ts index 4033c2636..967f7c20a 100644 --- a/web/app/components/header/facet-dropdown.ts +++ b/web/app/components/header/facet-dropdown.ts @@ -6,6 +6,8 @@ import { assert } from "@ember/debug"; import { restartableTask } from "ember-concurrency"; import { schedule } from "@ember/runloop"; import { FocusDirection } from "../x/hds/dropdown-list"; +import { inject as service } from "@ember/service"; +import RouterService from "@ember/routing/router-service"; interface FacetDropdownComponentSignature { Args: { @@ -16,6 +18,8 @@ interface FacetDropdownComponentSignature { } export default class FacetDropdownComponent extends Component { + @service declare router: RouterService; + @tracked private _triggerElement: HTMLButtonElement | null = null; @tracked private _scrollContainer: HTMLElement | null = null; @tracked private _popoverElement: HTMLDivElement | null = null; @@ -43,6 +47,10 @@ export default class FacetDropdownComponent extends Component diff --git a/web/app/components/x/hds/dropdown-list/item.ts b/web/app/components/x/hds/dropdown-list/item.ts index b4fd3afe1..9f6e2a40b 100644 --- a/web/app/components/x/hds/dropdown-list/item.ts +++ b/web/app/components/x/hds/dropdown-list/item.ts @@ -9,10 +9,11 @@ import { next } from "@ember/runloop"; interface XHdsDropdownListItemComponentSignature { Args: { - role?: string; - value?: any; - count?: number; + role: string; selected: boolean; + attrs?: unknown; + value: string; + count?: number; focusedItemIndex: number; listItemRole: string; hideDropdown: () => void; @@ -48,6 +49,10 @@ export default class XHdsDropdownListItemComponent extends Component {{#if @shownItems}} - {{#each-in @shownItems as |item|}} + {{#each-in @shownItems as |item attrs|}} { @@ -37,6 +38,7 @@ export default class XHdsDropdownListItemsComponent extends Component< return Object.entries(this.args.shownItems).length === 0; } + @action protected maybeKeyboardNavigate(event: KeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); From 8d8e3ccc4aadbe7226bdc886b04e11defa765603 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 20 Apr 2023 15:36:12 -0400 Subject: [PATCH 030/128] Yielded components; general refactors --- web/app/components/header/facet-dropdown.hbs | 39 ++----------------- web/app/components/header/toolbar.hbs | 5 ++- web/app/components/inputs/product-select.hbs | 16 ++------ .../components/x/hds/dropdown-list/action.hbs | 14 +++++++ .../components/x/hds/dropdown-list/item.hbs | 28 ++++++++++++- .../components/x/hds/dropdown-list/item.ts | 22 +---------- .../components/x/hds/dropdown-list/items.hbs | 8 ++-- .../x/hds/dropdown-list/link-to.hbs | 16 ++++++++ 8 files changed, 72 insertions(+), 76 deletions(-) create mode 100644 web/app/components/x/hds/dropdown-list/action.hbs create mode 100644 web/app/components/x/hds/dropdown-list/link-to.hbs diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 63c640dff..6d72ff05f 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -31,21 +31,10 @@ /> <:item as |i|> - {{log i}} - @@ -54,28 +43,6 @@ @count={{i.attrs.count}} @selected={{i.attrs.selected}} /> - + -{{!-- -{{#if this.dropdownIsShown}} - -{{/if}} --}} diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index dea6ba5ab..e441b7828 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -33,7 +33,9 @@
        {{#if (and @facets (not @sortControlIsHidden))}} - + --> {{/if}}
        diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index ec6204191..efd3ad81c 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -4,7 +4,7 @@ @items={{this.products}} @listIsOrdered={{true}} @onItemClick={{this.onChange}} - {{!-- This sort of expects a single select --}} + {{! This sort of expects a single select }} @selected={{@selected}} class="max-h-[320px] w-80" > @@ -33,23 +33,13 @@ <:item as |i|> - + - + {{else if this.fetchProducts.isRunning}} diff --git a/web/app/components/x/hds/dropdown-list/action.hbs b/web/app/components/x/hds/dropdown-list/action.hbs new file mode 100644 index 000000000..ed6c8154a --- /dev/null +++ b/web/app/components/x/hds/dropdown-list/action.hbs @@ -0,0 +1,14 @@ + + {{yield}} + diff --git a/web/app/components/x/hds/dropdown-list/item.hbs b/web/app/components/x/hds/dropdown-list/item.hbs index e358b34f2..4753a3076 100644 --- a/web/app/components/x/hds/dropdown-list/item.hbs +++ b/web/app/components/x/hds/dropdown-list/item.hbs @@ -1,3 +1,29 @@
      5. - {{yield this}} + {{log @attributes}} + {{yield + (hash + Action=(component + "x/hds/dropdown-list/action" + role=@role + isAriaSelected=this.isAriaSelected + selected=@selected + registerElement=this.registerElement + focusMouseTarget=this.focusMouseTarget + onClick=this.onClick + ) + LinkTo=(component + "x/hds/dropdown-list/link-to" + role=@role + isAriaSelected=this.isAriaSelected + selected=@selected + registerElement=this.registerElement + focusMouseTarget=this.focusMouseTarget + onClick=this.onClick + ) + selected=@selected + value=@value + count=@count + attrs=@attributes + ) + }}
      6. diff --git a/web/app/components/x/hds/dropdown-list/item.ts b/web/app/components/x/hds/dropdown-list/item.ts index 9f6e2a40b..f5989e5ce 100644 --- a/web/app/components/x/hds/dropdown-list/item.ts +++ b/web/app/components/x/hds/dropdown-list/item.ts @@ -11,7 +11,7 @@ interface XHdsDropdownListItemComponentSignature { Args: { role: string; selected: boolean; - attrs?: unknown; + attributes?: unknown; value: string; count?: number; focusedItemIndex: number; @@ -33,26 +33,6 @@ export default class XHdsDropdownListItemComponent extends Component - No matches -
        +
        + No matches +
        {{/if}} {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} @@ -22,7 +22,7 @@ + {{yield}} + From 8dde84507a2a7876a9d5666b36261e76349fde7e Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 20 Apr 2023 17:01:43 -0400 Subject: [PATCH 031/128] Toggle components --- web/app/components/header/facet-dropdown.hbs | 18 +--------- web/app/components/inputs/product-select.hbs | 17 ++-------- .../{checkable-item => checkable-item.ts} | 0 .../components/x/hds/dropdown-list/index.hbs | 33 ++++++++++++++++++- .../x/hds/dropdown-list/toggle-action.hbs | 11 +++++++ .../x/hds/dropdown-list/toggle-button.hbs | 14 ++++++++ 6 files changed, 61 insertions(+), 32 deletions(-) rename web/app/components/x/hds/dropdown-list/{checkable-item => checkable-item.ts} (100%) create mode 100644 web/app/components/x/hds/dropdown-list/toggle-action.hbs create mode 100644 web/app/components/x/hds/dropdown-list/toggle-button.hbs diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 6d72ff05f..75e9d1c63 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -12,23 +12,7 @@ {{! TODO: figure out "selected" }} > <:anchor as |d|> - + <:item as |i|> <:anchor as |d|> - + - + <:item as |i|> diff --git a/web/app/components/x/hds/dropdown-list/checkable-item b/web/app/components/x/hds/dropdown-list/checkable-item.ts similarity index 100% rename from web/app/components/x/hds/dropdown-list/checkable-item rename to web/app/components/x/hds/dropdown-list/checkable-item.ts diff --git a/web/app/components/x/hds/dropdown-list/index.hbs b/web/app/components/x/hds/dropdown-list/index.hbs index a2b9f81a0..73ca5327f 100644 --- a/web/app/components/x/hds/dropdown-list/index.hbs +++ b/web/app/components/x/hds/dropdown-list/index.hbs @@ -1,7 +1,38 @@ <:anchor as |f|> {{yield - (hash onTriggerKeydown=this.onTriggerKeydown floatingUI=f) + (hash + ToggleButton=(component + "x/hds/dropdown-list/toggle-button" + contentIsShown=f.contentIsShown + registerAnchor=f.registerAnchor + toggleContent=f.toggleContent + onTriggerKeydown=(fn this.onTriggerKeydown f) + color=(or @color "secondary") + disabled=@disabled + ariaControls=(concat + "x-hds-dropdown-list-" + (if this.inputIsShown "container" "list") + "-" + f.id + ) + text=@label + ) + ToggleAction=(component + "x/hds/dropdown-list/toggle-action" + registerAnchor=f.registerAnchor + onTriggerKeydown=(fn this.onTriggerKeydown f) + toggleContent=f.toggleContent + disabled=@disabled + ariaControls=(concat + "x-hds-dropdown-list-" + (if this.inputIsShown "container" "list") + "-" + f.id + ) + ) + contentIsShown=f.contentIsShown + ) to="anchor" }} diff --git a/web/app/components/x/hds/dropdown-list/toggle-action.hbs b/web/app/components/x/hds/dropdown-list/toggle-action.hbs new file mode 100644 index 000000000..c08fc26c7 --- /dev/null +++ b/web/app/components/x/hds/dropdown-list/toggle-action.hbs @@ -0,0 +1,11 @@ + + {{yield}} + diff --git a/web/app/components/x/hds/dropdown-list/toggle-button.hbs b/web/app/components/x/hds/dropdown-list/toggle-button.hbs new file mode 100644 index 000000000..f90e13b94 --- /dev/null +++ b/web/app/components/x/hds/dropdown-list/toggle-button.hbs @@ -0,0 +1,14 @@ + From 56486537e86d42c5f7543f424a12409516f78364 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Thu, 20 Apr 2023 17:11:16 -0400 Subject: [PATCH 032/128] Improved saving state --- web/app/components/document/sidebar.hbs | 12 +++++--- web/app/components/inputs/product-select.hbs | 29 ++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 0d2cfaf07..d131d5de4 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -134,10 +134,14 @@ class="hds-typography-body-100 hds-foreground-faint" >Product/Area {{#if this.isDraft}} - +
        + + +
        {{else}} <:anchor as |d|> - - - - +
        + {{#if @isSaving}} +
        + +
        + {{/if}} + + + + +
        <:item as |i|> From e94c00d8002b6a56a3b64589c631bda59f1d4f18 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 09:40:14 -0400 Subject: [PATCH 033/128] Class rearranging --- configs/config.hcl | 338 +++++++++--------- web/app/components/floating-u-i/content.hbs | 2 +- .../header/facet-dropdown-list-item.ts | 2 +- .../components/x/hds/dropdown-list/index.hbs | 6 +- web/app/router.js | 1 - web/app/styles/components/popover.scss | 11 +- 6 files changed, 177 insertions(+), 183 deletions(-) diff --git a/configs/config.hcl b/configs/config.hcl index b831c0297..5cae2fd3b 100644 --- a/configs/config.hcl +++ b/configs/config.hcl @@ -1,169 +1,169 @@ -// // base_url is the base URL used for building links. This should be the public -// // URL of the application. -// base_url = "http://localhost:8000" - -// // algolia configures Hermes to work with Algolia. -// algolia { -// application_id = "" -// docs_index_name = "docs" -// drafts_index_name = "drafts" -// internal_index_name = "internal" -// links_index_name = "links" -// missing_fields_index_name = "missing_fields" -// search_api_key = "" -// write_api_key = "" -// } - -// // document_types configures document types. Currently this block should not be -// // modified, but Hermes will support custom document types in the near future. -// // *** DO NOT MODIFY document_types *** -// document_types { -// document_type "RFC" { -// long_name = "Request for Comments" -// description = "Create a Request for Comments document to present a proposal to colleagues for their review and feedback." -// template = "1Oz_7FhaWxdFUDEzKCC5Cy58t57C4znmC_Qr80BORy1U" - -// more_info_link { -// text = "More info on the RFC template" -// url = "https://works.hashicorp.com/articles/rfc-template" -// } - -// custom_field { -// name = "Current Version" -// type = "string" -// } -// custom_field { -// name = "PRD" -// type = "string" -// } -// custom_field { -// name = "Stakeholders" -// type = "people" -// } -// custom_field { -// name = "Target Version" -// type = "string" -// } -// } - -// document_type "PRD" { -// long_name = "Product Requirements" -// description = "Create a Product Requirements Document to summarize a problem statement and outline a phased approach to addressing the problem." -// template = "1oS4q6IPDr3aMSTTk9UDdOnEcFwVWW9kT8ePCNqcg1P4" - -// more_info_link { -// text = "More info on the PRD template" -// url = "https://works.hashicorp.com/articles/prd-template" -// } - -// custom_field { -// name = "RFC" -// type = "string" -// } -// custom_field { -// name = "Stakeholders" -// type = "people" -// } -// } -// } - -// // email configures Hermes to send email notifications. -// email { -// // enabled enables sending email notifications. -// enabled = true - -// // from_address is the email address to send email notifications from. -// from_address = "hermes@yourorganization.com" -// } - -// // google_workspace configures Hermes to work with Google Workspace. -// google_workspace { -// // create_doc_shortcuts enables creating a shortcut in the shortcuts_folder -// // when a document is published. -// create_doc_shortcuts = true - -// // docs_folder contains all published documents in a flat structure. -// docs_folder = "my-docs-folder-id" - -// // drafts_folder contains all draft documents. -// drafts_folder = "my-drafts-folder-id" - -// // If create_doc_shortcuts is set to true, shortcuts_folder will contain an -// // organized hierarchy of folders and shortcuts to published files that can be -// // easily browsed directly in Google Drive: -// // {shortcut_folder}/{doc_type}/{product}/{document} -// shortcuts_folder = "my-shortcuts-folder-id" - -// // auth is the configuration for interacting with Google Workspace using a -// // service account. -// // auth { -// // client_email = "" -// // private_key = "" -// // subject = "" -// // token_url = "https://oauth2.googleapis.com/token" -// // } - -// // oauth2 is the configuration used to authenticate users via Google. -// oauth2 { -// client_id = "" -// hd = "hashicorp.com" -// redirect_uri = "http://localhost:8000/torii/redirect.html" -// } -// } - -// // indexer contains the configuration for the indexer. -// indexer { -// // max_parallel_docs is the maximum number of documents that will be -// // simultaneously indexed. -// max_parallel_docs = 5 - -// // update_doc_headers enables the indexer to automatically update document -// // headers for changed documents based on Hermes metadata. -// update_doc_headers = true - -// // update_draft_headers enables the indexer to automatically update document -// // headers for draft documents based on Hermes metadata. -// update_draft_headers = true -// } - -// // okta configures Hermes to authenticate users using an AWS Application Load -// // Balancer and Okta. -// okta { -// // auth_server_url is the URL of the Okta authorization server. -// auth_server_url = "" - -// // ClientID is the Okta client ID. -// client_id = "" - -// // disabled disables Okta authorization. -// disabled = true -// } - -// // postgres configures PostgreSQL as the app database. -// postgres { -// dbname = "db" -// host = "localhost" -// password = "postgres" -// port = 5432 -// user = "postgres" -// } - -// // products should be modified to reflect the products/areas in your -// // organization. -// products { -// product "Engineering" { -// abbreviation = "ENG" -// } -// product "Labs" { -// abbreviation = "LAB" -// } -// product "MyProduct" { -// abbreviation = "MY" -// } -// } - -// // server contains the configuration for the server. -// server { -// // addr is the address to bind to for listening. -// addr = "127.0.0.1:8000" -// } +// base_url is the base URL used for building links. This should be the public +// URL of the application. +base_url = "http://localhost:8000" + +// algolia configures Hermes to work with Algolia. +algolia { + application_id = "" + docs_index_name = "docs" + drafts_index_name = "drafts" + internal_index_name = "internal" + links_index_name = "links" + missing_fields_index_name = "missing_fields" + search_api_key = "" + write_api_key = "" +} + +// document_types configures document types. Currently this block should not be +// modified, but Hermes will support custom document types in the near future. +// *** DO NOT MODIFY document_types *** +document_types { + document_type "RFC" { + long_name = "Request for Comments" + description = "Create a Request for Comments document to present a proposal to colleagues for their review and feedback." + template = "1Oz_7FhaWxdFUDEzKCC5Cy58t57C4znmC_Qr80BORy1U" + + more_info_link { + text = "More info on the RFC template" + url = "https://works.hashicorp.com/articles/rfc-template" + } + + custom_field { + name = "Current Version" + type = "string" + } + custom_field { + name = "PRD" + type = "string" + } + custom_field { + name = "Stakeholders" + type = "people" + } + custom_field { + name = "Target Version" + type = "string" + } + } + + document_type "PRD" { + long_name = "Product Requirements" + description = "Create a Product Requirements Document to summarize a problem statement and outline a phased approach to addressing the problem." + template = "1oS4q6IPDr3aMSTTk9UDdOnEcFwVWW9kT8ePCNqcg1P4" + + more_info_link { + text = "More info on the PRD template" + url = "https://works.hashicorp.com/articles/prd-template" + } + + custom_field { + name = "RFC" + type = "string" + } + custom_field { + name = "Stakeholders" + type = "people" + } + } +} + +// email configures Hermes to send email notifications. +email { + // enabled enables sending email notifications. + enabled = true + + // from_address is the email address to send email notifications from. + from_address = "hermes@yourorganization.com" +} + +// google_workspace configures Hermes to work with Google Workspace. +google_workspace { + // create_doc_shortcuts enables creating a shortcut in the shortcuts_folder + // when a document is published. + create_doc_shortcuts = true + + // docs_folder contains all published documents in a flat structure. + docs_folder = "my-docs-folder-id" + + // drafts_folder contains all draft documents. + drafts_folder = "my-drafts-folder-id" + + // If create_doc_shortcuts is set to true, shortcuts_folder will contain an + // organized hierarchy of folders and shortcuts to published files that can be + // easily browsed directly in Google Drive: + // {shortcut_folder}/{doc_type}/{product}/{document} + shortcuts_folder = "my-shortcuts-folder-id" + + // auth is the configuration for interacting with Google Workspace using a + // service account. + // auth { + // client_email = "" + // private_key = "" + // subject = "" + // token_url = "https://oauth2.googleapis.com/token" + // } + + // oauth2 is the configuration used to authenticate users via Google. + oauth2 { + client_id = "" + hd = "hashicorp.com" + redirect_uri = "http://localhost:8000/torii/redirect.html" + } +} + +// indexer contains the configuration for the indexer. +indexer { + // max_parallel_docs is the maximum number of documents that will be + // simultaneously indexed. + max_parallel_docs = 5 + + // update_doc_headers enables the indexer to automatically update document + // headers for changed documents based on Hermes metadata. + update_doc_headers = true + + // update_draft_headers enables the indexer to automatically update document + // headers for draft documents based on Hermes metadata. + update_draft_headers = true +} + +// okta configures Hermes to authenticate users using an AWS Application Load +// Balancer and Okta. +okta { + // auth_server_url is the URL of the Okta authorization server. + auth_server_url = "" + + // ClientID is the Okta client ID. + client_id = "" + + // disabled disables Okta authorization. + disabled = true +} + +// postgres configures PostgreSQL as the app database. +postgres { + dbname = "db" + host = "localhost" + password = "postgres" + port = 5432 + user = "postgres" +} + +// products should be modified to reflect the products/areas in your +// organization. +products { + product "Engineering" { + abbreviation = "ENG" + } + product "Labs" { + abbreviation = "LAB" + } + product "MyProduct" { + abbreviation = "MY" + } +} + +// server contains the configuration for the server. +server { + // addr is the address to bind to for listening. + addr = "127.0.0.1:8000" +} diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index af16b8be6..05164fc7d 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -8,7 +8,7 @@ {{did-insert this.didInsert}} data-test-floating-ui-placement={{@placement}} id="floating-ui-content-{{this.id}}" - class="hermes-popover" + class="hermes-floating-ui-content" ...attributes > {{yield}} diff --git a/web/app/components/header/facet-dropdown-list-item.ts b/web/app/components/header/facet-dropdown-list-item.ts index 3f0918d44..bf3336778 100644 --- a/web/app/components/header/facet-dropdown-list-item.ts +++ b/web/app/components/header/facet-dropdown-list-item.ts @@ -2,11 +2,11 @@ import { action } from "@ember/object"; import RouterService from "@ember/routing/router-service"; import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; +import { FocusDirection } from "../x/hds/dropdown-list"; import { tracked } from "@glimmer/tracking"; import { assert } from "@ember/debug"; import { next } from "@ember/runloop"; import { SortByLabel, SortByValue } from "./toolbar"; -import { FocusDirection } from "../x/hds/dropdown-list"; enum FacetDropdownAriaRole { Option = "option", diff --git a/web/app/components/x/hds/dropdown-list/index.hbs b/web/app/components/x/hds/dropdown-list/index.hbs index 73ca5327f..e0d2497b8 100644 --- a/web/app/components/x/hds/dropdown-list/index.hbs +++ b/web/app/components/x/hds/dropdown-list/index.hbs @@ -1,4 +1,8 @@ - + <:anchor as |f|> {{yield (hash diff --git a/web/app/router.js b/web/app/router.js index c8a3612aa..a4eb30c50 100644 --- a/web/app/router.js +++ b/web/app/router.js @@ -20,6 +20,5 @@ Router.map(function () { }); }); this.route("authenticate"); - this.route("playground"); this.route("404", { path: "/*path" }); }); diff --git a/web/app/styles/components/popover.scss b/web/app/styles/components/popover.scss index be5d2bd6c..ae373c573 100644 --- a/web/app/styles/components/popover.scss +++ b/web/app/styles/components/popover.scss @@ -1,14 +1,5 @@ .hermes-popover { - @apply absolute bg-color-foreground-high-contrast rounded z-50 hds-dropdown__content; - // hermes dropdown list styles - - /* These positioning styles are overwritten by FloatingUI, - * but they ensure that the tooltip isn't added to the bottom of the page, - * where it could cause a reflow. This is especially important because - * the Google Docs iframe responds to layout changes and might - * otherwise jitter when a tooltip opened. - */ - @apply top-0 left-0; + @apply bg-color-foreground-high-contrast rounded z-50 hds-dropdown__content; .text { @apply relative; From b61b709c0fa95fcb4e122c22b9227adc5b8abf98 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 10:35:11 -0400 Subject: [PATCH 034/128] Add sortBy component --- web/app/components/header/facet-dropdown.hbs | 15 ++++---- web/app/components/header/sort-dropdown.hbs | 20 +++++++++++ web/app/components/header/sort-dropdown.ts | 34 +++++++++++++++++++ web/app/components/header/toolbar.hbs | 8 ++--- web/app/components/header/toolbar.ts | 10 +++++- .../x/hds/dropdown-list/link-to.hbs | 2 +- 6 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 web/app/components/header/sort-dropdown.hbs create mode 100644 web/app/components/header/sort-dropdown.ts diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 75e9d1c63..d5c5c6cd1 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -11,22 +11,19 @@ @listIsOrdered={{true}} {{! TODO: figure out "selected" }} > - <:anchor as |d|> - + <:anchor as |dd|> + - <:item as |i|> - + - + diff --git a/web/app/components/header/sort-dropdown.hbs b/web/app/components/header/sort-dropdown.hbs new file mode 100644 index 000000000..d132e8852 --- /dev/null +++ b/web/app/components/header/sort-dropdown.hbs @@ -0,0 +1,20 @@ + + <:anchor as |dd|> + + + <:item as |dd|> + + + {{dd.value}} + + + diff --git a/web/app/components/header/sort-dropdown.ts b/web/app/components/header/sort-dropdown.ts new file mode 100644 index 000000000..dd0f855ad --- /dev/null +++ b/web/app/components/header/sort-dropdown.ts @@ -0,0 +1,34 @@ +import Component from "@glimmer/component"; +import { SortByFacets, SortByLabel, SortByValue } from "./toolbar"; +import { inject as service } from "@ember/service"; +import RouterService from "@ember/routing/router-service"; + +interface HeaderSortDropdownComponentSignature { + Args: { + label: string; + facets: SortByFacets; + disabled: boolean; + currentSortByValue: SortByValue; + }; +} + +export default class HeaderSortDropdownComponent extends Component { + @service declare router: RouterService; + + get currentRouteName() { + return this.router.currentRouteName; + } + + get dateDesc() { + return SortByValue.DateDesc; + } + + get dateAsc() { + return SortByValue.DateAsc; + } + + get newestLabel() { + debugger; + return SortByLabel.Newest; + } +} diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index e441b7828..a4a4a6f2d 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -33,17 +33,13 @@
        {{#if (and @facets (not @sortControlIsHidden))}} - {{! TODO }} - {{/if}}
    diff --git a/web/app/components/header/toolbar.ts b/web/app/components/header/toolbar.ts index bea03986c..cdf220cd7 100644 --- a/web/app/components/header/toolbar.ts +++ b/web/app/components/header/toolbar.ts @@ -27,6 +27,13 @@ export enum FacetName { Product = "product", } +export interface SortByFacets { + [name: string]: { + count: number; + selected: boolean; + }; +} + export type ActiveFilters = { [name in FacetName]: string[]; }; @@ -57,6 +64,7 @@ export default class ToolbarComponent extends Component Date: Fri, 21 Apr 2023 10:39:54 -0400 Subject: [PATCH 035/128] Remove unused files --- .../header/facet-dropdown-list-item.hbs | 45 --- .../header/facet-dropdown-list-item.ts | 187 ------------ .../components/header/facet-dropdown-list.hbs | 69 ----- .../components/header/facet-dropdown-list.ts | 167 ----------- web/app/components/header/facet-dropdown.hbs | 22 +- web/app/components/header/sort-dropdown.ts | 7 +- .../components/x/hds/dropdown-list/index.hbs | 8 + .../header/facet-dropdown-list-item-test.ts | 168 ----------- .../header/facet-dropdown-list-test.ts | 270 ------------------ .../components/header/facet-dropdown-test.ts | 144 ---------- 10 files changed, 14 insertions(+), 1073 deletions(-) delete mode 100644 web/app/components/header/facet-dropdown-list-item.hbs delete mode 100644 web/app/components/header/facet-dropdown-list-item.ts delete mode 100644 web/app/components/header/facet-dropdown-list.hbs delete mode 100644 web/app/components/header/facet-dropdown-list.ts delete mode 100644 web/tests/integration/components/header/facet-dropdown-list-item-test.ts delete mode 100644 web/tests/integration/components/header/facet-dropdown-list-test.ts delete mode 100644 web/tests/integration/components/header/facet-dropdown-test.ts diff --git a/web/app/components/header/facet-dropdown-list-item.hbs b/web/app/components/header/facet-dropdown-list-item.hbs deleted file mode 100644 index 9d71fa658..000000000 --- a/web/app/components/header/facet-dropdown-list-item.hbs +++ /dev/null @@ -1,45 +0,0 @@ -
  • - - {{#if this.sortByQueryParams}} - - {{else}} - - {{/if}} -
    - - {{@value}} - - {{#unless this.sortByQueryParams}} - - {{/unless}} -
    -
    -
  • diff --git a/web/app/components/header/facet-dropdown-list-item.ts b/web/app/components/header/facet-dropdown-list-item.ts deleted file mode 100644 index bf3336778..000000000 --- a/web/app/components/header/facet-dropdown-list-item.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { action } from "@ember/object"; -import RouterService from "@ember/routing/router-service"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; -import { FocusDirection } from "../x/hds/dropdown-list"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { next } from "@ember/runloop"; -import { SortByLabel, SortByValue } from "./toolbar"; - -enum FacetDropdownAriaRole { - Option = "option", - Menuitem = "menuitem", -} - -interface HeaderFacetDropdownListItemComponentSignature { - Element: HTMLAnchorElement; - Args: { - /** - * The label of the facet, e.g., "Type" or "Owner." - * Used to construct filter query params. - */ - label: string; - /** - * The index of the currently focused item. - * Used to determine aria-focus. - */ - focusedItemIndex: number; - /** - * The name of the facet, e.g., "Approved," "In-Review." - * Used primarily to construct the query params. - */ - value: string; - /** - * The role of the list item, e.g., "option" or "menuitem". - * The if the facetDropdown has a filter input, the role is "option". - * Otherwise, it's "menuitem". - */ - role: FacetDropdownAriaRole; - /** - * The number of matches associated with the filter. - * Used to display the badge count. - */ - count: number; - /** - * Whether the item an actively applied filter. - * Used for checkmark-display logic, and as - * params for the `get-facet-query-hash` helper - */ - selected: boolean; - /** - * If the dropdown list is the sort control, the current sort value. - * Used to determine whether to use the `get-facet-query-hash` helper - * or this class's sortByQueryParams getter. - */ - currentSortByValue?: SortByValue; - /** - * The action called to hide the dropdown. - * Used to close the dropdown on the next run loop. - */ - hideDropdown: () => void; - /** - * The action called on mouseenter that sets the focused-item index value. - * Includes a `maybeScrollIntoView` argument that we use to disable - * mouse-activated scroll manipulation. - */ - setFocusedItemIndex: ( - focusDirection: FocusDirection | number, - maybeScrollIntoView?: boolean - ) => void; - }; -} - -export default class HeaderFacetDropdownListItemComponent extends Component { - @service declare router: RouterService; - - /** - * The element reference, set on insertion and updated on mouseenter. - * Used to compute the element's ID, which may change when the list is filtered. - */ - @tracked private _domElement: HTMLElement | null = null; - - /** - * An asserted-true reference to the element. - */ - protected get domElement() { - assert("element must exist", this._domElement); - return this._domElement; - } - - /** - * The element's domID, e.g., "facet-dropdown-list-item-0" - * Which is computed by the parent component on render and when - * the FacetList is filtered. Parsed by `this.id` to get the - * numeric identifier for the element. - */ - private get domElementID() { - return this.domElement.id; - } - - /** - * The current route name, used to set the LinkTo's @route - */ - protected get currentRouteName(): string { - return this.router.currentRouteName; - } - - /** - * A numeric identifier for the element based on its id, - * as computed by the parent component on render and when - * the FacetList is filtered. Strips everything but the trailing number. - * Used to apply classes and aria-selected, and to direct the parent component's - * focus action toward the correct element. - * Regex reference: - * \d = Any digit 0-9 - * + = One or more of the preceding token - * $ = End of input - */ - protected get itemIndexNumber(): number { - return parseInt(this.domElementID.match(/\d+$/)?.[0] || "0", 10); - } - - /** - * Whether the element is aria-selected. - * Used to determine whether to apply the "focused" class - * and to set the `aria-selected` attribute. - */ - protected get isAriaSelected(): boolean { - if (!this._domElement) { - // True when first computed, which happens - // before the element is inserted and registered. - return false; - } - if (this.args.focusedItemIndex === -1) { - return false; - } - return this.args.focusedItemIndex === this.itemIndexNumber; - } - - /** - * The query hash to use when the a sortBy filter is selected. - */ - protected get sortByQueryParams(): { sortBy: SortByValue } | void { - // The sortBy filter is the only facet that passes this argument. - if (!this.args.currentSortByValue) { - return; - } else { - switch (this.args.value) { - case SortByLabel.Newest: - return { sortBy: SortByValue.DateDesc }; - case SortByLabel.Oldest: - return { sortBy: SortByValue.DateAsc }; - } - } - } - - /** - * Sets our local `element` reference to mouse target, - * to capture its ID, which may change when the list is filtered. - * Then, calls the parent component's `setFocusedItemIndex` action, - * directing focus to the current element. - */ - @action protected focusMouseTarget(e: MouseEvent) { - let target = e.target; - assert("target must be an element", target instanceof HTMLElement); - this._domElement = target; - this.args.setFocusedItemIndex(this.itemIndexNumber, false); - } - - /** - * Closes the dropdown on the next run loop. - * Done so we don't interfere with Ember's handling. - */ - @action protected delayedCloseDropdown() { - next(() => { - this.args.hideDropdown(); - }); - } - - /** - * The action called on element insertion. Sets the local `element` - * reference to the domElement we know to be our target. - */ - @action protected registerElement(element: HTMLElement) { - this._domElement = element; - } -} diff --git a/web/app/components/header/facet-dropdown-list.hbs b/web/app/components/header/facet-dropdown-list.hbs deleted file mode 100644 index cb4fc6014..000000000 --- a/web/app/components/header/facet-dropdown-list.hbs +++ /dev/null @@ -1,69 +0,0 @@ -{{on-document "keydown" this.maybeKeyboardNavigate}} -
    - {{#if @inputIsShown}} -
    - -
    - {{/if}} -
    - {{#if this.noMatchesFound}} -
    - No matches -
    - {{else}} -
      - {{#each-in @shownFacets as |value attrs|}} - - {{/each-in}} -
    - {{/if}} -
    -
    diff --git a/web/app/components/header/facet-dropdown-list.ts b/web/app/components/header/facet-dropdown-list.ts deleted file mode 100644 index bcfcab801..000000000 --- a/web/app/components/header/facet-dropdown-list.ts +++ /dev/null @@ -1,167 +0,0 @@ -import Component from "@glimmer/component"; -import { FacetDropdownObjects } from "hermes/types/facets"; -import { action } from "@ember/object"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { inject as service } from "@ember/service"; -import RouterService from "@ember/routing/router-service"; -import { FocusDirection } from "../x/hds/dropdown-list"; - -interface HeaderFacetDropdownListComponentSignature { - Args: { - /** - * The facet's label, e.g., "Type," "Status." - * Used to construct facet query hashes. - */ - label: string; - /** - * Whether the facet dropdown has a filter input. - * Used to set the correct aria role for the containers, lists, and list items, - * and to determine the correct className for the dropdown. - */ - inputIsShown: boolean; - /** - * The facets that should be shown in the dropdown. - * Looped through in the tamplate to render the list items. - * Used to determine whether a "No matches found" message should be shown. - */ - shownFacets: FacetDropdownObjects; - /** - * The popover element, registered when its element is inserted. - * Used to scope our querySelector calls. - */ - popoverElement: HTMLDivElement | null; - /** - * The role of the list items. - * Used in querySelector calls to specify the correct list items. - */ - listItemRole: "option" | "menuitem"; - /** - * An action called to reset the focusedItem index. - * Used on input focusin, which happens on dropdown reveal. - **/ - resetFocusedItemIndex: () => void; - /** - * The action run when the user types in the filter input. - * Used to filter the shownFacets. - */ - onInput: (event: InputEvent) => void; - /** - * The action run when the popover is inserted into the DOM. - * Used to register the popover element for use in querySelector calls. - */ - registerPopover: (element: HTMLDivElement) => void; - /** - * The action to move the focusedItemIndex within the dropdown. - * Used on ArrowUp/ArrowDown/Enter keydown events, - * and to pass to our child element for mouseenter events. - */ - setFocusedItemIndex: (direction: FocusDirection) => void; - /** - * The action to hide the dropdown. - * Called when the user presses the Enter key on a selection, - * and passed to our child element for click events. - */ - hideDropdown: () => void; - }; -} - -export enum FacetNames { - DocType = "docType", - Owners = "owners", - Status = "status", - Product = "product", -} - -export default class HeaderFacetDropdownListComponent extends Component { - @service declare router: RouterService; - - /** - * The input element, registered when its element is inserted - * and asserted to exist in the inputElement getter. - */ - @tracked private _inputElement: HTMLInputElement | null = null; - - /** - * The name of the current route. - * Used to determine the component's LinkTo route. - */ - protected get currentRouteName(): string { - return this.router.currentRouteName; - } - - /** - * The input element. - * Receives focus when the user presses the up arrow key - * while the first menu item is focused. - */ - private get inputElement(): HTMLInputElement { - assert("_inputElement must exist", this._inputElement); - return this._inputElement; - } - - /** - * Whether there are no matches found for the user's input. - * Used to show a "No matches found" message in the template. - */ - protected get noMatchesFound(): boolean { - if (!this.args.inputIsShown) { - return false; - } - return Object.entries(this.args.shownFacets).length === 0; - } - - /** - * The code-friendly name of the facet. - * Used to apply width styles to the dropdown. - */ - protected get facetName(): FacetNames | undefined { - switch (this.args.label) { - case "Type": - return FacetNames.DocType; - case "Status": - return FacetNames.Status; - case "Product/Area": - return FacetNames.Product; - case "Owner": - return FacetNames.Owners; - } - } - - /** - * Registers the input element. - * Used to assert that the element exists and can be focused. - */ - @action protected registerAndFocusInput(element: HTMLInputElement) { - this._inputElement = element; - this.inputElement.focus(); - } - - /** - * The action run when the user presses a key. - * Handles the arrow keys to navigate the dropdown or - * hits Enter to select the focused item. - */ - @action protected maybeKeyboardNavigate(event: KeyboardEvent) { - if (event.key === "ArrowDown") { - event.preventDefault(); - this.args.setFocusedItemIndex(FocusDirection.Next); - } - - if (event.key === "ArrowUp") { - event.preventDefault(); - this.args.setFocusedItemIndex(FocusDirection.Previous); - } - - if (event.key === "Enter") { - event.preventDefault(); - assert("popoverElement must exist", this.args.popoverElement); - const target = this.args.popoverElement.querySelector("[aria-selected]"); - - if (target instanceof HTMLAnchorElement) { - target.click(); - this.args.hideDropdown(); - } - } - } -} diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index d5c5c6cd1..d20d3d26a 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -1,28 +1,16 @@ -{{! - Marked up with guidance from: - https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ - https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/ - https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/ - https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions-active-descendant/ -}} - - + <:anchor as |dd|> <:item as |dd|> diff --git a/web/app/components/header/sort-dropdown.ts b/web/app/components/header/sort-dropdown.ts index dd0f855ad..3b2142f21 100644 --- a/web/app/components/header/sort-dropdown.ts +++ b/web/app/components/header/sort-dropdown.ts @@ -1,5 +1,5 @@ import Component from "@glimmer/component"; -import { SortByFacets, SortByLabel, SortByValue } from "./toolbar"; +import { SortByFacets, SortByValue } from "./toolbar"; import { inject as service } from "@ember/service"; import RouterService from "@ember/routing/router-service"; @@ -26,9 +26,4 @@ export default class HeaderSortDropdownComponent extends Component {}); - }); - - test("the check mark is visible if the filter is selected", async function (assert) { - this.set("isSelected", false); - - await render(hbs` - - `); - - assert - .dom("[data-test-facet-dropdown-list-item-check]") - .hasStyle({ visibility: "hidden" }, "check is initially hidden"); - - this.set("isSelected", true); - - assert - .dom("[data-test-facet-dropdown-list-item-check]") - .hasStyle( - { visibility: "visible" }, - 'check is visible when "selected" is true' - ); - }); - - test("filters display a badge count and sort controls show an icon", async function (assert) { - this.set("currentSortByValue", undefined); - - await render(hbs` - - `); - - assert - .dom("[data-test-facet-dropdown-menu-item-count]") - .hasText("15", "badge count is displayed"); - assert - .dom("[data-test-facet-dropdown-list-item-sort-icon]") - .doesNotExist( - "sort icon isn't shown unless `currentSortByValue` is defined" - ); - - this.set("currentSortByValue", SortByValue.DateAsc); - - assert - .dom("[data-test-facet-dropdown-list-item-sort-icon]") - .exists("sort icon is shown"); - assert - .dom("[data-test-facet-dropdown-menu-item-count]") - .doesNotExist( - "badge count isn't shown when `currentSortByValue` is defined" - ); - }); - - test("it has the correct queryParams", async function (assert) { - this.set("currentSortByValue", null); - this.set("value", "Approved"); - - const activeFiltersService = this.owner.lookup( - "service:active-filters" - ) as ActiveFiltersService; - - activeFiltersService.index = { - docType: [], - owners: [], - product: [], - status: [], - }; - - await render(hbs` - - `); - - assert - .dom("[data-test-facet-dropdown-menu-item-link]") - .hasAttribute( - "href", - "/all?status=%5B%22Approved%22%5D", - "filter queryParams are correct" - ); - - this.set("currentSortByValue", SortByValue.DateAsc); - this.set("value", "Oldest"); - - assert - .dom("[data-test-facet-dropdown-menu-item-link]") - .hasAttribute( - "href", - `/all?sortBy=${SortByValue.DateAsc}`, - "sort queryParams are correct" - ); - }); - - test("it gets aria-focused on mouseenter", async function (assert) { - this.set("isSelected", false); - this.set("focusedItemIndex", -1); - this.set("setFocusedItemIndex", (focusDirection: number) => { - this.set("focusedItemIndex", focusDirection); - }); - - await render(hbs` - - `); - - const listItemSelector = "[data-test-facet-dropdown-menu-item-link]"; - - assert.dom(listItemSelector).doesNotHaveClass("is-aria-selected"); - assert.dom(listItemSelector).doesNotHaveAttribute("aria-selected"); - - await triggerEvent(listItemSelector, "mouseenter"); - assert.dom(listItemSelector).hasClass("is-aria-selected"); - assert.dom(listItemSelector).hasAttribute("aria-selected"); - }); - } -); diff --git a/web/tests/integration/components/header/facet-dropdown-list-test.ts b/web/tests/integration/components/header/facet-dropdown-list-test.ts deleted file mode 100644 index 35e2f6c3a..000000000 --- a/web/tests/integration/components/header/facet-dropdown-list-test.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "ember-qunit"; -import { find, findAll, render, triggerKeyEvent } from "@ember/test-helpers"; -import { assert as emberAssert } from "@ember/debug"; -import { hbs } from "ember-cli-htmlbars"; -import { LONG_FACET_LIST, SHORT_FACET_LIST } from "./facet-dropdown-test"; -import { FocusDirection } from "hermes/components/x/hds/dropdown-list"; - -module( - "Integration | Component | header/facet-dropdown-list", - function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("popoverElement", null); - this.set("registerPopover", (element: HTMLDivElement) => { - this.set("popoverElement", element); - }); - this.set("focusedItemIndex", -1); - this.set("scrollContainer", null); - this.set("registerScrollContainer", (element: HTMLDivElement) => { - this.set("scrollContainer", element); - }); - this.set("resetFocusedItemIndex", () => { - this.set("focusedItemIndex", -1); - }); - this.set("onInput", () => {}); - this.set("setFocusedItemIndex", (direction: FocusDirection) => { - const currentIndex = this.get("focusedItemIndex"); - emberAssert( - "currentIndex must be a number", - typeof currentIndex === "number" - ); - - const numberOfItems = findAll( - "[data-test-facet-dropdown-menu-item]" - ).length; - - if (direction === FocusDirection.Next) { - if (currentIndex === numberOfItems - 1) { - this.set("focusedItemIndex", 0); - return; - } else { - this.set("focusedItemIndex", currentIndex + 1); - return; - } - } - if (direction === FocusDirection.Previous) { - if (currentIndex === 0) { - this.set("focusedItemIndex", numberOfItems - 1); - return; - } else { - this.set("focusedItemIndex", currentIndex - 1); - return; - } - } - }); - }); - - test("keyboard navigation works as expected (long list)", async function (assert) { - this.set("shownFacets", LONG_FACET_LIST); - - await render(hbs` - - `); - - let inputSelector = "[data-test-facet-dropdown-input]"; - let input = find(inputSelector); - - emberAssert("input must exist", input); - - assert.equal(document.activeElement, input, "The input is autofocused"); - assert - .dom("[data-test-facet-dropdown-popover]") - .hasAttribute("role", "combobox"); - assert - .dom(inputSelector) - .doesNotHaveAttribute( - "aria-activedescendant", - "No items are aria-focused yet" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "When no items are aria-focused, ArrowDown moves aria-focus to the first item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "ArrowDown moves aria-focus to the next item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowUp"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "ArrowUp moves aria-focus to the previous item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowUp"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-12", - "Keying up on the first item aria-focuses the last item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "Keying down on the last item aria-focuses the first item" - ); - }); - - test("keyboard navigation works as expected (short list)", async function (assert) { - this.set("shownFacets", SHORT_FACET_LIST); - - await render(hbs` - - `); - - let menuSelector = "[data-test-facet-dropdown-menu]"; - let menu = find(menuSelector); - - emberAssert("menu must exist", menu); - - assert - .dom(menuSelector) - .doesNotHaveAttribute( - "aria-activedescendant", - "No items are aria-focused yet" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowDown"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "When no items are aria-focused, ArrowDown moves aria-focus to the first item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowDown"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "ArrowDown moves aria-focus to the next item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowUp"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "ArrowUp moves aria-focus to the previous item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowUp"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "Keying up on the first item aria-focuses the last item" - ); - }); - - test("it applies the correct classNames to the popover", async function (assert) { - this.set("shownFacets", SHORT_FACET_LIST); - this.set("label", "Status"); - this.set("inputIsShown", false); - - await render(hbs` - - `); - - let popoverSelector = "[data-test-facet-dropdown-popover]"; - let popover = find(popoverSelector); - - emberAssert("popover must exist", popover); - - assert.dom(popoverSelector).doesNotHaveClass("large"); - assert - .dom(popoverSelector) - .hasClass("medium", 'the status facet has a "medium" class'); - - this.set("label", "Type"); - - assert.dom(popoverSelector).doesNotHaveClass("large"); - assert - .dom(popoverSelector) - .doesNotHaveClass( - "medium", - 'only the status facet has a "medium" class' - ); - - this.set("inputIsShown", true); - - assert - .dom(popoverSelector) - .hasClass("large", 'facets with inputs have a "large" class'); - - this.set("label", "Status"); - - assert.dom(popoverSelector).hasClass("large"); - assert - .dom(popoverSelector) - .doesNotHaveClass( - "medium", - "because the status facet has an input, the medium class is not applied" - ); - }); - } -); diff --git a/web/tests/integration/components/header/facet-dropdown-test.ts b/web/tests/integration/components/header/facet-dropdown-test.ts deleted file mode 100644 index d70ee1526..000000000 --- a/web/tests/integration/components/header/facet-dropdown-test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "ember-qunit"; -import { click, fillIn, render, triggerKeyEvent } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; - -export const SHORT_FACET_LIST = { - RFC: { - count: 10, - selected: false, - }, - PRD: { - count: 5, - selected: true, - }, -}; - -export const LONG_FACET_LIST = { - Filter01: { count: 1, selected: false }, - Filter02: { count: 1, selected: false }, - Filter03: { count: 1, selected: false }, - Filter04: { count: 1, selected: false }, - Filter05: { count: 1, selected: false }, - Filter06: { count: 1, selected: false }, - Filter07: { count: 1, selected: false }, - Filter08: { count: 1, selected: false }, - Filter09: { count: 1, selected: false }, - Filter10: { count: 1, selected: false }, - Filter11: { count: 1, selected: false }, - Filter12: { count: 1, selected: false }, - Filter13: { count: 1, selected: false }, -}; - -module("Integration | Component | header/facet-dropdown", function (hooks) { - setupRenderingTest(hooks); - - test("it toggles when the trigger is clicked", async function (assert) { - this.set("facets", SHORT_FACET_LIST); - await render(hbs` - - `); - assert.dom("[data-test-facet-dropdown-popover]").doesNotExist(); - await click("[data-test-facet-dropdown-trigger]"); - assert.dom("[data-test-facet-dropdown-popover]").exists("The dropdown is shown"); - }); - - test("it renders the facets correctly", async function (assert) { - this.set("facets", SHORT_FACET_LIST); - await render(hbs` - - `); - await click("[data-test-facet-dropdown-trigger]"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(1)") - .hasText("RFC 10", "Correct facet name and count"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(1) .flight-icon") - .hasStyle({ visibility: "hidden" }, "Unselected facets have no icon"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2)") - .hasText("PRD 5", "Correct facet name and count"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2) .flight-icon") - .hasStyle({ visibility: "visible" }, "Selected facets have an icon"); - }); - - test("an input is shown when there are more than 12 facets", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - - `); - await click("[data-test-facet-dropdown-trigger]"); - assert.dom("[data-test-facet-dropdown-input]").exists("The input is shown"); - }); - - test("filtering works as expected", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - - `); - - await click("[data-test-facet-dropdown-trigger]"); - - let firstItemSelector = "#facet-dropdown-menu-item-0"; - - assert.dom(firstItemSelector).hasText("Filter01 1"); - assert.dom("[data-test-facet-dropdown-menu-item]").exists({ count: 13 }); - await fillIn("[data-test-facet-dropdown-input]", "3"); - - assert - .dom("[data-test-facet-dropdown-menu-item]") - .exists({ count: 2 }, "The facets are filtered"); - assert - .dom(firstItemSelector) - .hasText( - "Filter03 1", - "The facet IDs are updated when the list is filtered to match the new order" - ); - - await fillIn("[data-test-facet-dropdown-input]", "foobar"); - - assert.dom("[data-test-facet-dropdown-menu]").doesNotExist(); - assert - .dom("[data-test-facet-dropdown-menu-empty-state]") - .exists('the "No matches" message is shown'); - }); - - test("popover trigger has keyboard support", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - - `); - - assert.dom("[data-test-facet-dropdown-popover]").doesNotExist(); - - await triggerKeyEvent( - "[data-test-facet-dropdown-trigger]", - "keydown", - "ArrowDown" - ); - - assert.dom("[data-test-facet-dropdown-popover]").exists("The dropdown is shown"); - let firstItemSelector = "#facet-dropdown-menu-item-0"; - - assert.dom(firstItemSelector).hasAttribute("aria-selected"); - assert - .dom("[data-test-facet-dropdown-menu]") - .hasAttribute("aria-activedescendant", "facet-dropdown-menu-item-0"); - }); -}); From 7faa80551f5dc1acc7559ca1dce147b3ec2d1448 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 12:10:15 -0400 Subject: [PATCH 036/128] Update syntax --- web/app/components/inputs/product-select.hbs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 1066d580e..330680ad5 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,21 +1,21 @@ {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} + {{#if this.products}} - <:anchor as |d|> + <:anchor as |dd|>
    {{#if @isSaving}}
    {{/if}} - + - +
    - <:item as |i|> - + <:item as |dd|> + - +
    {{else if this.fetchProducts.isRunning}} From 2628d73179ca961080fdc82482f9976042cc5590 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 12:28:16 -0400 Subject: [PATCH 037/128] Improve tests passing --- web/app/components/header/toolbar.hbs | 2 +- web/app/components/header/toolbar.ts | 9 ++++++++- web/app/components/x/hds/dropdown-list/index.ts | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index a4a4a6f2d..3ea2de00f 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -1,4 +1,4 @@ -{{#if @facets}} +{{#if this.facetsAreShown}}
    diff --git a/web/app/components/header/toolbar.ts b/web/app/components/header/toolbar.ts index cdf220cd7..9ff323904 100644 --- a/web/app/components/header/toolbar.ts +++ b/web/app/components/header/toolbar.ts @@ -64,7 +64,7 @@ export default class ToolbarComponent extends Component 0; + } + /** * Whether the owner facet is disabled. * True on the My Docs and My Drafts screens. diff --git a/web/app/components/x/hds/dropdown-list/index.ts b/web/app/components/x/hds/dropdown-list/index.ts index 7a77afc5e..cc94edcc6 100644 --- a/web/app/components/x/hds/dropdown-list/index.ts +++ b/web/app/components/x/hds/dropdown-list/index.ts @@ -52,7 +52,11 @@ export default class XHdsDropdownListComponent extends Component< } get inputIsShown() { - return Object.keys(this.args.items).length > 7; + if (!this.args.items) { + return false; + } else { + return Object.keys(this.args.items).length > 7; + } } get shownItems() { From 341bd211e6e953a5e2ac70148e27565577dc64e7 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 13:19:35 -0400 Subject: [PATCH 038/128] Update tests --- web/app/components/floating-u-i/content.hbs | 2 +- web/app/components/floating-u-i/content.ts | 9 +++--- web/app/components/floating-u-i/index.hbs | 5 +++- web/app/components/floating-u-i/index.ts | 3 ++ web/app/components/header/facet-dropdown.hbs | 2 +- web/app/components/header/sort-dropdown.hbs | 12 ++++++-- web/app/components/header/toolbar.hbs | 2 +- web/app/components/header/toolbar.ts | 7 ----- .../components/x/hds/dropdown-list/item.hbs | 1 - .../components/floating-u-i/content-test.ts | 17 +++++++++-- .../components/floating-u-i/index-test.ts | 1 - .../components/header/toolbar-test.ts | 30 +++++++++---------- 12 files changed, 54 insertions(+), 37 deletions(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index 05164fc7d..5327e13fb 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -7,7 +7,7 @@ {{will-destroy this.cleanup}} {{did-insert this.didInsert}} data-test-floating-ui-placement={{@placement}} - id="floating-ui-content-{{this.id}}" + id="floating-ui-content-{{@id}}" class="hermes-floating-ui-content" ...attributes > diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 61a29c0c8..3f18794f5 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -16,24 +16,25 @@ import { tracked } from "@glimmer/tracking"; interface FloatingUIContentSignature { Args: { anchor: HTMLElement; + content: HTMLElement | null; placement?: Placement; renderOut?: boolean; + registerContent: (e: HTMLElement) => void; }; } export default class FloatingUIContent extends Component { readonly id = guidFor(this); - @tracked _content: HTMLElement | null = null; @tracked cleanup: (() => void) | null = null; get content() { - assert("_content must exist", this._content); - return this._content; + assert("_this.args.content must exist", this.args.content); + return this.args.content; } @action didInsert(e: HTMLElement) { - this._content = e; + this.args.registerContent(e); let updatePosition = async () => { computePosition(this.args.anchor, this.content, { diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index 6c89af21c..e580cf688 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -5,9 +5,12 @@ @placement={{@placement}} @renderOut={{@renderOut}} @anchor={{this.anchor}} - {{did-insert this.registerContent}} + @content={{this.content}} + @id={{this.contentID}} + @registerContent={{this.registerContent}} ...attributes > + {{!-- TODO: Make this yield more --}} {{yield this to="content"}} {{/if}} diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index e2be2f76d..810b15b7b 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -1,5 +1,6 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; +import { guidFor } from "@ember/object/internals"; import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; @@ -12,6 +13,8 @@ interface FloatingUIComponentSignature { } export default class FloatingUIComponent extends Component { + readonly contentID = guidFor(this); + @tracked _anchor: HTMLElement | null = null; @tracked content: HTMLElement | null = null; @tracked contentIsShown: boolean = false; diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index d20d3d26a..51652278a 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -1,6 +1,6 @@ <:anchor as |dd|> - + <:item as |dd|> + <:anchor as |dd|> - + <:item as |dd|>
    diff --git a/web/app/components/header/toolbar.ts b/web/app/components/header/toolbar.ts index 9ff323904..02797da16 100644 --- a/web/app/components/header/toolbar.ts +++ b/web/app/components/header/toolbar.ts @@ -76,13 +76,6 @@ export default class ToolbarComponent extends Component 0; - } - /** * Whether the owner facet is disabled. * True on the My Docs and My Drafts screens. diff --git a/web/app/components/x/hds/dropdown-list/item.hbs b/web/app/components/x/hds/dropdown-list/item.hbs index 4753a3076..0e7d7d6c9 100644 --- a/web/app/components/x/hds/dropdown-list/item.hbs +++ b/web/app/components/x/hds/dropdown-list/item.hbs @@ -1,5 +1,4 @@
  • - {{log @attributes}} {{yield (hash Action=(component diff --git a/web/tests/integration/components/floating-u-i/content-test.ts b/web/tests/integration/components/floating-u-i/content-test.ts index 65461f684..6c256a994 100644 --- a/web/tests/integration/components/floating-u-i/content-test.ts +++ b/web/tests/integration/components/floating-u-i/content-test.ts @@ -11,6 +11,13 @@ const CONTENT_OFFSET = 5; module("Integration | Component | floating-u-i/content", function (hooks) { setupRenderingTest(hooks); + hooks.beforeEach(function (this: TestContext) { + this.set("content", null); + this.set("registerContent", (e: HTMLElement) => { + this.set("content", e); + }); + }); + test("it can be rendered inline or outside", async function (assert) { this.set("renderOut", undefined); @@ -21,9 +28,11 @@ module("Integration | Component | floating-u-i/content", function (hooks) {
    Content @@ -73,9 +82,10 @@ module("Integration | Component | floating-u-i/content", function (hooks) {
    Content @@ -109,10 +119,11 @@ module("Integration | Component | floating-u-i/content", function (hooks) { Attach
  • Content diff --git a/web/tests/integration/components/floating-u-i/index-test.ts b/web/tests/integration/components/floating-u-i/index-test.ts index cd8ea29d8..7908e11b9 100644 --- a/web/tests/integration/components/floating-u-i/index-test.ts +++ b/web/tests/integration/components/floating-u-i/index-test.ts @@ -41,7 +41,6 @@ module("Integration | Component | floating-u-i/index", function (hooks) { assert.dom(".content").exists(); const contentID = htmlElement(".content").id; - assert.ok(contentID.startsWith("ember"), "a contentID was assigned"); await click(".open-button"); diff --git a/web/tests/integration/components/header/toolbar-test.ts b/web/tests/integration/components/header/toolbar-test.ts index 9b4676a97..b7bcb2ce8 100644 --- a/web/tests/integration/components/header/toolbar-test.ts +++ b/web/tests/integration/components/header/toolbar-test.ts @@ -1,9 +1,8 @@ import { module, test, todo } from "qunit"; import { setupRenderingTest } from "ember-qunit"; -import { click, find, findAll, render } from "@ember/test-helpers"; +import { click, findAll, render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { FacetDropdownObjects } from "hermes/types/facets"; -import RouterService from "@ember/routing/router-service"; import { SortByLabel } from "hermes/components/header/toolbar"; const FACETS = { @@ -46,27 +45,28 @@ module("Integration | Component | header/toolbar", function (hooks) { assert.dom(".facets").exists(); assert - .dom("[data-test-facet-dropdown='sort']") + .dom("[data-test-header-sort-dropdown-trigger]") .exists("Sort-by dropdown is shown with facets unless explicitly hidden"); assert .dom(".facets [data-test-facet-dropdown-trigger]") .exists({ count: 4 }); assert - .dom('[data-test-facet-dropdown="sort"]') + .dom("[data-test-header-sort-dropdown-trigger]") .exists({ count: 1 }) .hasText(`Sort: ${SortByLabel.Newest}`); - await click( - `[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']` - ); + await click(`[data-test-header-sort-dropdown-trigger]`); + assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2)") + .dom( + "[data-test-header-sort-by-dropdown] .x-hds-dropdown-list-item:nth-child(2)" + ) .hasText("Oldest"); this.set("sortControlIsHidden", true); assert - .dom(".sort-by-dropdown") + .dom("[data-test-header-sort-by-dropdown]") .doesNotExist("Sort-by dropdown hides when sortByHidden is true"); }); @@ -98,9 +98,9 @@ module("Integration | Component | header/toolbar", function (hooks) { await click("[data-test-facet-dropdown-trigger='Status']"); assert.deepEqual( - findAll( - "[data-test-facet-dropdown-menu-item] [data-test-facet-dropdown-list-item-value]" - )?.map((el) => el.textContent?.trim()), + findAll(".x-hds-dropdown-list-item-value")?.map((el) => + el.textContent?.trim() + ), ["Approved", "In-Review", "In Review", "Obsolete", "WIP"], "Unsupported statuses are filtered out" ); @@ -123,12 +123,12 @@ module("Integration | Component | header/toolbar", function (hooks) { `); assert - .dom(`[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']`) + .dom(`[data-test-header-sort-dropdown-trigger]`) .doesNotHaveAttribute("disabled"); - this.set("facets", {}); + this.set("facets", {}); assert - .dom(`[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']`) + .dom(`[data-test-header-sort-dropdown-trigger]`) .hasAttribute("disabled"); }); From fdfc88005db759465c288bfcdb2d495051169089 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 14:37:27 -0400 Subject: [PATCH 039/128] FloatingUI API tweaks --- web/app/components/floating-u-i/content.ts | 9 ++--- web/app/components/floating-u-i/index.hbs | 22 +++++++++-- web/app/components/floating-u-i/index.ts | 1 + web/app/components/header/toolbar.ts | 1 - .../components/x/hds/dropdown-list/index.hbs | 30 +++++++------- .../components/x/hds/dropdown-list/index.ts | 39 +++++++++---------- .../components/x/hds/dropdown-list/items.hbs | 2 +- .../components/x/hds/dropdown-list/items.ts | 11 +++--- 8 files changed, 63 insertions(+), 52 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 3f18794f5..5a801d71f 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -16,10 +16,8 @@ import { tracked } from "@glimmer/tracking"; interface FloatingUIContentSignature { Args: { anchor: HTMLElement; - content: HTMLElement | null; placement?: Placement; renderOut?: boolean; - registerContent: (e: HTMLElement) => void; }; } @@ -27,14 +25,15 @@ export default class FloatingUIContent extends Component void) | null = null; + @tracked _content: HTMLElement | null = null; get content() { - assert("_this.args.content must exist", this.args.content); - return this.args.content; + assert("_content must exist", this._content); + return this._content; } @action didInsert(e: HTMLElement) { - this.args.registerContent(e); + this._content = e; let updatePosition = async () => { computePosition(this.args.anchor, this.content, { diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index e580cf688..ec9460593 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -1,4 +1,14 @@ -{{yield this to="anchor"}} +{{yield + (hash + contentIsShown=this.contentIsShown + registerAnchor=this.registerAnchor + toggleContent=this.toggleContent + showContent=this.showContent + hideContent=this.hideContent + contentID=this.contentID + ) + to="anchor" +}} {{#if this.contentIsShown}} - {{!-- TODO: Make this yield more --}} - {{yield this to="content"}} + {{yield + (hash + contentID=this.contentID hideContent=this.hideContent anchor=this.anchor + ) + to="content" + }} {{/if}} diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 810b15b7b..1c236da14 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -30,6 +30,7 @@ export default class FloatingUIComponent extends Component <:content as |f|>
    - {{! So the problem is we want the flexibility to use LinkTos or Actions, - each perhaps with its own unique requirements. Need to find some way of yielding the list item with a quick way of tagging the interactive component with stuff }} <:item as |i|> {{yield i to="item"}} -
    - {{! action list }} - {{! link list }}
    diff --git a/web/app/components/x/hds/dropdown-list/index.ts b/web/app/components/x/hds/dropdown-list/index.ts index cc94edcc6..d304e19b9 100644 --- a/web/app/components/x/hds/dropdown-list/index.ts +++ b/web/app/components/x/hds/dropdown-list/index.ts @@ -4,7 +4,7 @@ import { next, schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; -import { restartableTask, task } from "ember-concurrency"; +import { restartableTask } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; interface XHdsDropdownListComponentSignature { @@ -83,17 +83,13 @@ export default class XHdsDropdownListComponent extends Component< this.input.focus(); } - @action protected didInsertList(f: any) { - schedule( - "afterRender", - () => { - assert("floatingUI content must exist", f.content); - this.assignMenuItemIDs( - f.content.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }, - f - ); + @action protected didInsertList() { + schedule("afterRender", () => { + assert("didInsertList expects a _scrollContainer", this._scrollContainer); + this.assignMenuItemIDs( + this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) + ); + }); } @action willDestroyDropdown() { @@ -190,17 +186,20 @@ export default class XHdsDropdownListComponent extends Component< } } - @action protected onTriggerKeydown(f: any, event: KeyboardEvent) { - if (f.contentIsShown) { + @action protected onTriggerKeydown( + contentIsShown: boolean, + showContent: () => void, + event: KeyboardEvent + ) { + if (contentIsShown) { return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { + // Stop the event from bubbling to the popover's keydown handler. event.preventDefault(); - f.showContent(); - // Stop the event from bubbling to the popover's keydown handler. - // event.stopPropagation(); + showContent(); // Wait for the menuItems to be set by the showDropdown action. next(() => { @@ -217,8 +216,7 @@ export default class XHdsDropdownListComponent extends Component< } protected onInput = restartableTask( - async (f: any, inputEvent: InputEvent) => { - console.log(f); + async (content: HTMLElement | null, inputEvent: InputEvent) => { this.focusedItemIndex = -1; let showItems: any = {}; @@ -234,8 +232,9 @@ export default class XHdsDropdownListComponent extends Component< this.filteredItems = showItems; schedule("afterRender", () => { + assert("onInput expects floatingUI content", content); this.assignMenuItemIDs( - f.content.querySelectorAll(`[role=${this.listItemRole}]`) + content.querySelectorAll(`[role=${this.listItemRole}]`) ); }); } diff --git a/web/app/components/x/hds/dropdown-list/items.hbs b/web/app/components/x/hds/dropdown-list/items.hbs index 5381191a4..4c53a562c 100644 --- a/web/app/components/x/hds/dropdown-list/items.hbs +++ b/web/app/components/x/hds/dropdown-list/items.hbs @@ -24,7 +24,7 @@ @value={{item}} @attributes={{attrs}} @selected={{eq @selected item}} - @hideDropdown={{@f.hideContent}} + @hideDropdown={{@hideContent}} @onItemClick={{@onItemClick}} @setFocusedItemIndex={{@setFocusedItemIndex}} @focusedItemIndex={{@focusedItemIndex}} diff --git a/web/app/components/x/hds/dropdown-list/items.ts b/web/app/components/x/hds/dropdown-list/items.ts index 0379579e1..9dbeefa7c 100644 --- a/web/app/components/x/hds/dropdown-list/items.ts +++ b/web/app/components/x/hds/dropdown-list/items.ts @@ -1,7 +1,6 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; interface XHdsDropdownListItemsComponentSignature { @@ -18,7 +17,8 @@ interface XHdsDropdownListItemsComponentSignature { resetFocusedItemIndex: () => void; registerScrollContainer?: (e: HTMLElement) => void; setFocusedItemIndex: (direction: FocusDirection) => void; - f: any; + content: HTMLElement | null; + hideContent: () => void; }; } @@ -38,7 +38,6 @@ export default class XHdsDropdownListItemsComponent extends Component< return Object.entries(this.args.shownItems).length === 0; } - @action protected maybeKeyboardNavigate(event: KeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); @@ -52,15 +51,15 @@ export default class XHdsDropdownListItemsComponent extends Component< if (event.key === "Enter") { event.preventDefault(); - assert("floatingUI content must exist", this.args.f.content); - const target = this.args.f.content.querySelector("[aria-selected]"); + assert("floatingUI content must exist", this.args.content); + const target = this.args.content.querySelector("[aria-selected]"); if ( target instanceof HTMLAnchorElement || target instanceof HTMLButtonElement ) { target.click(); - this.args.f.hideContent(); + this.args.hideContent(); } } } From 51f99aca8c11307cd6bd18ad33b9736237205797 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 14:43:33 -0400 Subject: [PATCH 040/128] Tweak FUI --- web/app/components/floating-u-i/content.ts | 5 +---- web/app/components/floating-u-i/index.hbs | 2 -- web/app/components/floating-u-i/index.ts | 5 ----- .../components/floating-u-i/content-test.ts | 14 -------------- .../components/floating-u-i/index-test.ts | 1 + 5 files changed, 2 insertions(+), 25 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 5a801d71f..62c6d0ee1 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -1,6 +1,5 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; -import { guidFor } from "@ember/object/internals"; import { Placement, autoUpdate, @@ -22,10 +21,8 @@ interface FloatingUIContentSignature { } export default class FloatingUIContent extends Component { - readonly id = guidFor(this); - - @tracked cleanup: (() => void) | null = null; @tracked _content: HTMLElement | null = null; + @tracked cleanup: (() => void) | null = null; get content() { assert("_content must exist", this._content); diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index ec9460593..61679e116 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -15,8 +15,6 @@ @placement={{@placement}} @renderOut={{@renderOut}} @anchor={{this.anchor}} - @content={{this.content}} - @registerContent={{this.registerContent}} @id={{this.contentID}} ...attributes > diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 1c236da14..aeea89c09 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -28,11 +28,6 @@ export default class FloatingUIComponent extends Component { - this.set("content", e); - }); - }); - test("it can be rendered inline or outside", async function (assert) { this.set("renderOut", undefined); @@ -28,11 +21,8 @@ module("Integration | Component | floating-u-i/content", function (hooks) {
    Content @@ -84,8 +74,6 @@ module("Integration | Component | floating-u-i/content", function (hooks) { style="width: 100px" @anchor={{html-element '.anchor'}} @placement="left" - @content={{this.content}} - @registerContent={{this.registerContent}} > Content @@ -122,8 +110,6 @@ module("Integration | Component | floating-u-i/content", function (hooks) { style="width: 100px" @anchor={{html-element '.anchor'}} @placement="right" - @content={{this.content}} - @registerContent={{this.registerContent}} > Content diff --git a/web/tests/integration/components/floating-u-i/index-test.ts b/web/tests/integration/components/floating-u-i/index-test.ts index 7908e11b9..cd8ea29d8 100644 --- a/web/tests/integration/components/floating-u-i/index-test.ts +++ b/web/tests/integration/components/floating-u-i/index-test.ts @@ -41,6 +41,7 @@ module("Integration | Component | floating-u-i/index", function (hooks) { assert.dom(".content").exists(); const contentID = htmlElement(".content").id; + assert.ok(contentID.startsWith("ember"), "a contentID was assigned"); await click(".open-button"); From d5b53640ea5bbe155128f48084903d242ab3c5d4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 15:04:18 -0400 Subject: [PATCH 041/128] Delete playground.hbs --- web/app/templates/playground.hbs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 web/app/templates/playground.hbs diff --git a/web/app/templates/playground.hbs b/web/app/templates/playground.hbs deleted file mode 100644 index e69de29bb..000000000 From 6f167553f8d34c778111f68060f2aa093339c125 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 15:09:00 -0400 Subject: [PATCH 042/128] Update config.ts --- web/mirage/config.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/mirage/config.ts b/web/mirage/config.ts index 0e3c31a4f..d0d8630c2 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -220,7 +220,11 @@ export default function (mirageConfig) { // 1: { "Vault": { abbreviation: "VLT"} } // ] - // We need to reformat them to match the API's response. + // We reformat them to match the API's response: + // { + // "Labs": { abbreviation: "LAB" }, + // "Vault": { abbreviation: "VLT" } + // } let formattedObjects = {}; @@ -229,12 +233,6 @@ export default function (mirageConfig) { formattedObjects[key] = object[key]; }); - // The formattedObjects now look look like: - // { - // "Labs": { abbreviation: "LAB" }, - // "Vault": { abbreviation: "VLT" } - // } - return new Response(200, {}, formattedObjects); }); From af7adf0d92d7554d1bad54a9fb657db59eb94c4e Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 15:16:42 -0400 Subject: [PATCH 043/128] Rename/reorg --- web/app/components/header/facet-dropdown.hbs | 6 ++--- web/app/components/header/facet-dropdown.ts | 2 +- web/app/components/header/sort-dropdown.hbs | 4 ++-- web/app/components/inputs/product-select.hbs | 6 ++--- .../x/{hds => }/dropdown-list/action.hbs | 2 +- .../dropdown-list/checkable-item.hbs | 4 ++-- .../x/dropdown-list/checkable-item.ts | 11 +++++++++ .../x/{hds => }/dropdown-list/index.hbs | 24 +++++++++---------- .../x/{hds => }/dropdown-list/index.ts | 8 +++---- .../x/{hds => }/dropdown-list/item.hbs | 6 ++--- .../x/{hds => }/dropdown-list/item.ts | 4 ++-- .../x/{hds => }/dropdown-list/items.hbs | 10 ++++---- .../x/{hds => }/dropdown-list/items.ts | 8 +++---- .../x/{hds => }/dropdown-list/link-to.hbs | 2 +- .../{hds => }/dropdown-list/toggle-action.hbs | 0 .../{hds => }/dropdown-list/toggle-button.hbs | 0 .../x/hds/dropdown-list/checkable-item.ts | 11 --------- web/app/styles/app.scss | 4 ++-- .../x/{hds => }/dropdown/list-item.scss | 8 +++---- .../components/x/{hds => }/dropdown/list.scss | 10 ++++---- .../components/header/toolbar-test.ts | 4 ++-- 21 files changed, 67 insertions(+), 67 deletions(-) rename web/app/components/x/{hds => }/dropdown-list/action.hbs (88%) rename web/app/components/x/{hds => }/dropdown-list/checkable-item.hbs (68%) create mode 100644 web/app/components/x/dropdown-list/checkable-item.ts rename web/app/components/x/{hds => }/dropdown-list/index.hbs (85%) rename web/app/components/x/{hds => }/dropdown-list/index.ts (97%) rename web/app/components/x/{hds => }/dropdown-list/item.hbs (82%) rename web/app/components/x/{hds => }/dropdown-list/item.ts (95%) rename web/app/components/x/{hds => }/dropdown-list/items.hbs (83%) rename web/app/components/x/{hds => }/dropdown-list/items.ts (86%) rename web/app/components/x/{hds => }/dropdown-list/link-to.hbs (89%) rename web/app/components/x/{hds => }/dropdown-list/toggle-action.hbs (100%) rename web/app/components/x/{hds => }/dropdown-list/toggle-button.hbs (100%) delete mode 100644 web/app/components/x/hds/dropdown-list/checkable-item.ts rename web/app/styles/components/x/{hds => }/dropdown/list-item.scss (80%) rename web/app/styles/components/x/{hds => }/dropdown/list.scss (56%) diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 51652278a..615008e2c 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -1,4 +1,4 @@ - + <:anchor as |dd|> @@ -7,11 +7,11 @@ @route={{this.currentRouteName}} @query={{get-facet-query-hash @label dd.value dd.attrs.selected}} > - - + diff --git a/web/app/components/header/facet-dropdown.ts b/web/app/components/header/facet-dropdown.ts index 967f7c20a..3f057fd0e 100644 --- a/web/app/components/header/facet-dropdown.ts +++ b/web/app/components/header/facet-dropdown.ts @@ -5,7 +5,7 @@ import { tracked } from "@glimmer/tracking"; import { assert } from "@ember/debug"; import { restartableTask } from "ember-concurrency"; import { schedule } from "@ember/runloop"; -import { FocusDirection } from "../x/hds/dropdown-list"; +import { FocusDirection } from "../x/dropdown-list"; import { inject as service } from "@ember/service"; import RouterService from "@ember/routing/router-service"; diff --git a/web/app/components/header/sort-dropdown.hbs b/web/app/components/header/sort-dropdown.hbs index 738186e48..7e528b542 100644 --- a/web/app/components/header/sort-dropdown.hbs +++ b/web/app/components/header/sort-dropdown.hbs @@ -1,4 +1,4 @@ - - + diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 330680ad5..cfcd093cd 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,7 +1,7 @@ {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} - <:item as |dd|> - - + {{else if this.fetchProducts.isRunning}} {{else}} diff --git a/web/app/components/x/hds/dropdown-list/action.hbs b/web/app/components/x/dropdown-list/action.hbs similarity index 88% rename from web/app/components/x/hds/dropdown-list/action.hbs rename to web/app/components/x/dropdown-list/action.hbs index ed6c8154a..09ba945b7 100644 --- a/web/app/components/x/hds/dropdown-list/action.hbs +++ b/web/app/components/x/dropdown-list/action.hbs @@ -6,7 +6,7 @@ aria-selected={{@isAriaSelected}} tabindex="-1" aria-checked={{@selected}} - class="x-hds-dropdown-list-item-link + class="x-dropdown-list-item-link {{if @isAriaSelected 'is-aria-selected'}}" ...attributes > diff --git a/web/app/components/x/hds/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs similarity index 68% rename from web/app/components/x/hds/dropdown-list/checkable-item.hbs rename to web/app/components/x/dropdown-list/checkable-item.hbs index 0cc22767a..82ba5c5c0 100644 --- a/web/app/components/x/hds/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -2,13 +2,13 @@ @name="check" class="check {{if @selected 'visible' 'invisible'}}" /> -
    +
    {{@value}}
    {{#if @count}} {{/if}} diff --git a/web/app/components/x/dropdown-list/checkable-item.ts b/web/app/components/x/dropdown-list/checkable-item.ts new file mode 100644 index 000000000..82061869d --- /dev/null +++ b/web/app/components/x/dropdown-list/checkable-item.ts @@ -0,0 +1,11 @@ +import Component from "@glimmer/component"; + +interface XDropdownListCheckableItemComponentSignature { + Args: { + selected: boolean; + value: string; + count?: number; + }; +} + +export default class XDropdownListCheckableItemComponent extends Component {} diff --git a/web/app/components/x/hds/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs similarity index 85% rename from web/app/components/x/hds/dropdown-list/index.hbs rename to web/app/components/x/dropdown-list/index.hbs index c286ded86..bd2dfc1ed 100644 --- a/web/app/components/x/hds/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -15,7 +15,7 @@ {{yield (hash ToggleButton=(component - "x/hds/dropdown-list/toggle-button" + "x/dropdown-list/toggle-button" contentIsShown=f.contentIsShown registerAnchor=f.registerAnchor toggleContent=f.toggleContent @@ -25,7 +25,7 @@ color=(or @color "secondary") disabled=@disabled ariaControls=(concat - "x-hds-dropdown-list-" + "x-dropdown-list-" (if this.inputIsShown "container" "list") "-" f.contentID @@ -33,7 +33,7 @@ text=@label ) ToggleAction=(component - "x/hds/dropdown-list/toggle-action" + "x/dropdown-list/toggle-action" registerAnchor=f.registerAnchor onTriggerKeydown=(fn this.onTriggerKeydown f.contentIsShown f.showContent @@ -41,7 +41,7 @@ toggleContent=f.toggleContent disabled=@disabled ariaControls=(concat - "x-hds-dropdown-list-" + "x-dropdown-list-" (if this.inputIsShown "container" "list") "-" f.contentID @@ -54,8 +54,8 @@ <:content as |f|>
    {{#if this.inputIsShown}} -
    +
    {{/if}}
    - {{yield i to="item"}} - +
    diff --git a/web/app/components/x/hds/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts similarity index 97% rename from web/app/components/x/hds/dropdown-list/index.ts rename to web/app/components/x/dropdown-list/index.ts index d304e19b9..3d9feb2d0 100644 --- a/web/app/components/x/hds/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -7,7 +7,7 @@ import { tracked } from "@glimmer/tracking"; import { restartableTask } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; -interface XHdsDropdownListComponentSignature { +interface XDropdownListComponentSignature { Args: { selected: any; items?: any; @@ -23,8 +23,8 @@ export enum FocusDirection { Last = "last", } -export default class XHdsDropdownListComponent extends Component< - XHdsDropdownListComponentSignature +export default class XDropdownListComponent extends Component< + XDropdownListComponentSignature > { @service("fetch") declare fetchSvc: FetchService; @@ -64,7 +64,7 @@ export default class XHdsDropdownListComponent extends Component< } get ariaControls() { - let value = "x-hds-dropdown-"; + let value = "x-dropdown-"; if (this.inputIsShown) { value += "popover"; } else { diff --git a/web/app/components/x/hds/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs similarity index 82% rename from web/app/components/x/hds/dropdown-list/item.hbs rename to web/app/components/x/dropdown-list/item.hbs index 0e7d7d6c9..2de63d036 100644 --- a/web/app/components/x/hds/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,8 +1,8 @@ -
  • +
  • {{yield (hash Action=(component - "x/hds/dropdown-list/action" + "x/dropdown-list/action" role=@role isAriaSelected=this.isAriaSelected selected=@selected @@ -11,7 +11,7 @@ onClick=this.onClick ) LinkTo=(component - "x/hds/dropdown-list/link-to" + "x/dropdown-list/link-to" role=@role isAriaSelected=this.isAriaSelected selected=@selected diff --git a/web/app/components/x/hds/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts similarity index 95% rename from web/app/components/x/hds/dropdown-list/item.ts rename to web/app/components/x/dropdown-list/item.ts index f5989e5ce..9783cd1d1 100644 --- a/web/app/components/x/hds/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -7,7 +7,7 @@ import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; import { next } from "@ember/runloop"; -interface XHdsDropdownListItemComponentSignature { +interface XDropdownListItemComponentSignature { Args: { role: string; selected: boolean; @@ -25,7 +25,7 @@ interface XHdsDropdownListItemComponentSignature { }; } -export default class XHdsDropdownListItemComponent extends Component { +export default class XDropdownListItemComponent extends Component { @service declare router: RouterService; /** * The element reference, set on insertion and updated on mouseenter. diff --git a/web/app/components/x/hds/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs similarity index 83% rename from web/app/components/x/hds/dropdown-list/items.hbs rename to web/app/components/x/dropdown-list/items.hbs index 4c53a562c..0b36a2aa0 100644 --- a/web/app/components/x/hds/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -5,21 +5,21 @@ {{#if (has-block "empty-state")}} {{yield to="empty-state"}} {{else}} -
    +
    No matches
    {{/if}} {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} {{#if @shownItems}} {{#each-in @shownItems as |item attrs|}} - {{yield i to="item"}} - + {{/each-in}} {{/if}} diff --git a/web/app/components/x/hds/dropdown-list/items.ts b/web/app/components/x/dropdown-list/items.ts similarity index 86% rename from web/app/components/x/hds/dropdown-list/items.ts rename to web/app/components/x/dropdown-list/items.ts index 9dbeefa7c..7f2bd1115 100644 --- a/web/app/components/x/hds/dropdown-list/items.ts +++ b/web/app/components/x/dropdown-list/items.ts @@ -3,7 +3,7 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { FocusDirection } from "."; -interface XHdsDropdownListItemsComponentSignature { +interface XDropdownListItemsComponentSignature { Args: { id: string; items?: any; @@ -22,12 +22,12 @@ interface XHdsDropdownListItemsComponentSignature { }; } -export default class XHdsDropdownListItemsComponent extends Component< - XHdsDropdownListItemsComponentSignature +export default class XDropdownListItemsComponent extends Component< + XDropdownListItemsComponentSignature > { get ariaActiveDescendant() { if (this.args.focusedItemIndex !== -1) { - return `x-hds-dropdown-list-item-${this.args.focusedItemIndex}`; + return `x-dropdown-list-item-${this.args.focusedItemIndex}`; } } diff --git a/web/app/components/x/hds/dropdown-list/link-to.hbs b/web/app/components/x/dropdown-list/link-to.hbs similarity index 89% rename from web/app/components/x/hds/dropdown-list/link-to.hbs rename to web/app/components/x/dropdown-list/link-to.hbs index 39108724b..660dd9fe3 100644 --- a/web/app/components/x/hds/dropdown-list/link-to.hbs +++ b/web/app/components/x/dropdown-list/link-to.hbs @@ -8,7 +8,7 @@ aria-checked={{@selected}} @route={{@route}} @query={{or @query (hash)}} - class="x-hds-dropdown-list-item-link + class="x-dropdown-list-item-link {{if @isAriaSelected 'is-aria-selected'}}" ...attributes > diff --git a/web/app/components/x/hds/dropdown-list/toggle-action.hbs b/web/app/components/x/dropdown-list/toggle-action.hbs similarity index 100% rename from web/app/components/x/hds/dropdown-list/toggle-action.hbs rename to web/app/components/x/dropdown-list/toggle-action.hbs diff --git a/web/app/components/x/hds/dropdown-list/toggle-button.hbs b/web/app/components/x/dropdown-list/toggle-button.hbs similarity index 100% rename from web/app/components/x/hds/dropdown-list/toggle-button.hbs rename to web/app/components/x/dropdown-list/toggle-button.hbs diff --git a/web/app/components/x/hds/dropdown-list/checkable-item.ts b/web/app/components/x/hds/dropdown-list/checkable-item.ts deleted file mode 100644 index 5df85f85e..000000000 --- a/web/app/components/x/hds/dropdown-list/checkable-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Component from "@glimmer/component"; - -interface XHdsDropdownListCheckableItemComponentSignature { - Args: { - selected: boolean; - value: string; - count?: number; - }; -} - -export default class XHdsDropdownListCheckableItemComponent extends Component {} diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 0e73d87da..d5da6f502 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -5,8 +5,8 @@ @use "components/footer"; @use "components/nav"; @use "components/x-hds-tab"; -@use "components/x/hds/dropdown/list"; -@use "components/x/hds/dropdown/list-item"; +@use "components/x/dropdown/list"; +@use "components/x/dropdown/list-item"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; diff --git a/web/app/styles/components/x/hds/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss similarity index 80% rename from web/app/styles/components/x/hds/dropdown/list-item.scss rename to web/app/styles/components/x/dropdown/list-item.scss index 2c5c95ad9..8fbddf68c 100644 --- a/web/app/styles/components/x/hds/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -1,8 +1,8 @@ -.x-hds-dropdown-list-item { +.x-dropdown-list-item { @apply flex; } -.x-hds-dropdown-list-item-link { +.x-dropdown-list-item-link { @apply no-underline flex items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; &.is-aria-selected { @@ -26,10 +26,10 @@ } } -.x-hds-dropdown-list-item-value { +.x-dropdown-list-item-value { @apply truncate whitespace-nowrap; } -.x-hds-dropdown-list-item-count { +.x-dropdown-list-item-count { @apply ml-8 shrink-0; } diff --git a/web/app/styles/components/x/hds/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss similarity index 56% rename from web/app/styles/components/x/hds/dropdown/list.scss rename to web/app/styles/components/x/dropdown/list.scss index 7e6206d86..5cec8a79d 100644 --- a/web/app/styles/components/x/hds/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -1,19 +1,19 @@ -.x-hds-dropdown-list-container { +.x-dropdown-list-container { @apply flex flex-col overflow-hidden; } -.x-hds-dropdown-list-input-container { +.x-dropdown-list-input-container { @apply relative p-1 border-b border-b-color-border-faint; } -.x-hds-dropdown-list-scroll-container { +.x-dropdown-list-scroll-container { @apply overflow-auto w-full relative; } -.x-hds-dropdown-list-empty-state { +.x-dropdown-list-empty-state { @apply p-12 text-center text-color-foreground-faint; } -.x-hds-dropdown-list { +.x-dropdown-list { @apply py-1; } diff --git a/web/tests/integration/components/header/toolbar-test.ts b/web/tests/integration/components/header/toolbar-test.ts index b7bcb2ce8..cc834f312 100644 --- a/web/tests/integration/components/header/toolbar-test.ts +++ b/web/tests/integration/components/header/toolbar-test.ts @@ -60,7 +60,7 @@ module("Integration | Component | header/toolbar", function (hooks) { assert .dom( - "[data-test-header-sort-by-dropdown] .x-hds-dropdown-list-item:nth-child(2)" + "[data-test-header-sort-by-dropdown] .x-dropdown-list-item:nth-child(2)" ) .hasText("Oldest"); @@ -98,7 +98,7 @@ module("Integration | Component | header/toolbar", function (hooks) { await click("[data-test-facet-dropdown-trigger='Status']"); assert.deepEqual( - findAll(".x-hds-dropdown-list-item-value")?.map((el) => + findAll(".x-dropdown-list-item-value")?.map((el) => el.textContent?.trim() ), ["Approved", "In-Review", "In Review", "Obsolete", "WIP"], From b157123d9abc99f37ca794d879e0e6ba1414b90c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 15:47:45 -0400 Subject: [PATCH 044/128] Remove references to `f.content` --- web/app/components/x/dropdown-list/index.hbs | 15 ++++---- web/app/components/x/dropdown-list/index.ts | 38 +++++++++----------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index bd2dfc1ed..4387ac549 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -54,26 +54,24 @@ <:content as |f|>
    {{#if this.inputIsShown}}
    <:item as |i|> diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 3d9feb2d0..9847dc470 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -28,7 +28,6 @@ export default class XDropdownListComponent extends Component< > { @service("fetch") declare fetchSvc: FetchService; - @tracked _trigger: HTMLElement | null = null; @tracked private _scrollContainer: HTMLElement | null = null; @tracked protected query: string = ""; @@ -70,7 +69,6 @@ export default class XDropdownListComponent extends Component< } else { value += "list"; } - return `${value}-`; } @@ -215,28 +213,26 @@ export default class XDropdownListComponent extends Component< } } - protected onInput = restartableTask( - async (content: HTMLElement | null, inputEvent: InputEvent) => { - this.focusedItemIndex = -1; + protected onInput = restartableTask(async (inputEvent: InputEvent) => { + this.focusedItemIndex = -1; - let showItems: any = {}; - let { items } = this.args; + let showItems: any = {}; + let { items } = this.args; - this.query = (inputEvent.target as HTMLInputElement).value; - for (const [key, value] of Object.entries(items)) { - if (key.toLowerCase().includes(this.query.toLowerCase())) { - showItems[key] = value; - } + this.query = (inputEvent.target as HTMLInputElement).value; + for (const [key, value] of Object.entries(items)) { + if (key.toLowerCase().includes(this.query.toLowerCase())) { + showItems[key] = value; } + } - this.filteredItems = showItems; + this.filteredItems = showItems; - schedule("afterRender", () => { - assert("onInput expects floatingUI content", content); - this.assignMenuItemIDs( - content.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); - } - ); + schedule("afterRender", () => { + assert("onInput expects a _scrollContainer", this._scrollContainer); + this.assignMenuItemIDs( + this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) + ); + }); + }); } From 91c9d884817e501acbac03b7199b18f7fb1871a0 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 16:11:30 -0400 Subject: [PATCH 045/128] Add comments to index --- web/app/components/x/dropdown-list/index.hbs | 4 +- web/app/components/x/dropdown-list/index.ts | 162 +++++++++++++------ 2 files changed, 115 insertions(+), 51 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index 4387ac549..919d08cff 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -54,8 +54,8 @@ <:content as |f|>
    | null = null; + @tracked protected query: string = ""; @tracked protected listItemRole = this.inputIsShown ? "option" : "menuitem"; - @tracked protected focusedItemIndex = -1; - @tracked filteredItems: unknown | null = null; - @tracked protected menuItems: NodeListOf | null = null; - - @tracked _input: HTMLInputElement | null = null; - + /** + * An asserted-true reference to the scroll container. + */ private get scrollContainer(): HTMLElement { assert("_scrollContainer must exist", this._scrollContainer); return this._scrollContainer; } + /** + * An asserted-true reference to the filter input. + */ get input() { assert("input must exist", this._input); return this._input; } + /** + * Whether the dropdown has a filter input. + * Used to determine layout configurations and + * aria-roles for various elements. + */ get inputIsShown() { if (!this.args.items) { return false; @@ -58,10 +66,18 @@ export default class XDropdownListComponent extends Component< } } + /** + * The items that should be shown in the dropdown. + * Initially the same as the items passed in and + * updated when the user types in the filter input. + */ get shownItems() { - return this.filteredItems || this.args.items; + return this._filteredItems || this.args.items; } + /** + * The "aria-controls" value for the dropdown trigger. + */ get ariaControls() { let value = "x-dropdown-"; if (this.inputIsShown) { @@ -72,30 +88,56 @@ export default class XDropdownListComponent extends Component< return `${value}-`; } + /** + * The action run when the scrollContainer is inserted. + * Registers the div for reference locally. + */ @action protected registerScrollContainer(element: HTMLDivElement) { this._scrollContainer = element; } + /** + * The action performed when the filter input is inserted. + * Registers the input locally and focuses it for the user. + */ @action registerAndFocusInput(e: HTMLInputElement) { this._input = e; this.input.focus(); } - @action protected didInsertList() { + /** + * The action run when the content div is inserted. + * Used to assign ids to the menu items. + */ + @action protected didInsertContent() { schedule("afterRender", () => { - assert("didInsertList expects a _scrollContainer", this._scrollContainer); + assert( + "didInsertContent expects a _scrollContainer", + this._scrollContainer + ); this.assignMenuItemIDs( this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) ); }); } - @action willDestroyDropdown() { - this.filteredItems = null; + /** + * The action run when the content div is destroyed. + * Resets the filtered items so that the next time the + * popover is opened, the full list of items is shown. + */ + @action resetFilteredItems() { + this._filteredItems = null; } + /** + * The action run when the popover is inserted, and when + * the user filters or navigates the dropdown. + * Loops through the menu items and assigns an id that + * matches the index of the item in the list. + */ @action assignMenuItemIDs(items: NodeListOf): void { - this.menuItems = items; + this._menuItems = items; for (let i = 0; i < items.length; i++) { let item = items[i]; assert("item must exist", item instanceof HTMLElement); @@ -103,11 +145,48 @@ export default class XDropdownListComponent extends Component< } } + /** + * The action run when the trigger is focused and the user + * presses the up or down arrow keys. Used to open and focus + * to the first or last item in the dropdown. + */ + @action protected onTriggerKeydown( + contentIsShown: boolean, + showContent: () => void, + event: KeyboardEvent + ) { + if (contentIsShown) { + return; + } + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault(); + showContent(); + + // Wait for the menuItems to be set by the showContent action. + next(() => { + switch (event.key) { + case "ArrowDown": + this.setFocusedItemIndex(FocusDirection.First, false); + break; + case "ArrowUp": + this.setFocusedItemIndex(FocusDirection.Last); + break; + } + }); + } + } + + /** + * Sets the focus to the next or previous menu item. + * Used by the onKeydown action to navigate the dropdown, and + * by the FacetDropdownListItem component on mouseenter.s + */ @action protected setFocusedItemIndex( focusDirectionOrNumber: FocusDirection | number, maybeScrollIntoView = true ) { - let { menuItems, focusedItemIndex } = this; + let { _menuItems: menuItems, focusedItemIndex } = this; let setFirst = () => { focusedItemIndex = 0; @@ -161,12 +240,13 @@ export default class XDropdownListComponent extends Component< } } - @action protected resetFocusedItemIndex() { - this.focusedItemIndex = -1; - } - + /** + * Checks whether the focused item is completely visible, + * and, if necessary, scrolls the dropdown to make it visible. + * Used by the setFocusedItemIndex action on keydown. + */ private maybeScrollIntoView() { - const focusedItem = this.menuItems?.item(this.focusedItemIndex); + const focusedItem = this._menuItems?.item(this.focusedItemIndex); assert("focusedItem must exist", focusedItem instanceof HTMLElement); const containerTopPadding = 12; @@ -184,49 +264,33 @@ export default class XDropdownListComponent extends Component< } } - @action protected onTriggerKeydown( - contentIsShown: boolean, - showContent: () => void, - event: KeyboardEvent - ) { - if (contentIsShown) { - return; - } - - if (event.key === "ArrowUp" || event.key === "ArrowDown") { - // Stop the event from bubbling to the popover's keydown handler. - event.preventDefault(); - - showContent(); - - // Wait for the menuItems to be set by the showDropdown action. - next(() => { - switch (event.key) { - case "ArrowDown": - this.setFocusedItemIndex(FocusDirection.First, false); - break; - case "ArrowUp": - this.setFocusedItemIndex(FocusDirection.Last); - break; - } - }); - } + /** + * Resets the focus index to its initial value. + * Called when the dropdown is closed, and when the input is focused. + */ + @action protected resetFocusedItemIndex() { + this.focusedItemIndex = -1; } + /** + * The action run when the user types in the input. + * Filters the facets shown in the dropdown and schedules + * the menu items to be assigned their new IDs. + */ protected onInput = restartableTask(async (inputEvent: InputEvent) => { this.focusedItemIndex = -1; - let showItems: any = {}; + let shownItems: any = {}; let { items } = this.args; this.query = (inputEvent.target as HTMLInputElement).value; for (const [key, value] of Object.entries(items)) { if (key.toLowerCase().includes(this.query.toLowerCase())) { - showItems[key] = value; + shownItems[key] = value; } } - this.filteredItems = showItems; + this._filteredItems = shownItems; schedule("afterRender", () => { assert("onInput expects a _scrollContainer", this._scrollContainer); From 6dd20c613e00b908609baa81c41e2a91e051c8bd Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 21 Apr 2023 16:36:39 -0400 Subject: [PATCH 046/128] Additional classes and documentation --- web/app/components/inputs/product-select.hbs | 1 - web/app/components/x/dropdown-list/action.hbs | 5 +-- web/app/components/x/dropdown-list/action.ts | 17 ++++++++ web/app/components/x/dropdown-list/index.hbs | 17 ++++---- web/app/components/x/dropdown-list/item.hbs | 15 ++++--- web/app/components/x/dropdown-list/item.ts | 6 +-- web/app/components/x/dropdown-list/items.hbs | 16 +++----- web/app/components/x/dropdown-list/items.ts | 40 ++++++++++++------- .../components/x/dropdown-list/link-to.hbs | 5 +-- web/app/components/x/dropdown-list/link-to.ts | 19 +++++++++ .../x/dropdown-list/toggle-action.ts | 14 +++++++ web/app/router.js | 2 +- 12 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 web/app/components/x/dropdown-list/action.ts create mode 100644 web/app/components/x/dropdown-list/link-to.ts create mode 100644 web/app/components/x/dropdown-list/toggle-action.ts diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index cfcd093cd..bffee44e0 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -33,7 +33,6 @@ diff --git a/web/app/components/x/dropdown-list/action.hbs b/web/app/components/x/dropdown-list/action.hbs index 09ba945b7..aa4e02b01 100644 --- a/web/app/components/x/dropdown-list/action.hbs +++ b/web/app/components/x/dropdown-list/action.hbs @@ -4,10 +4,9 @@ {{on "click" @onClick}} role={{@role}} aria-selected={{@isAriaSelected}} + aria-checked={{@isAriaChecked}} tabindex="-1" - aria-checked={{@selected}} - class="x-dropdown-list-item-link - {{if @isAriaSelected 'is-aria-selected'}}" + class="x-dropdown-list-item-link {{if @isAriaSelected 'is-aria-selected'}}" ...attributes > {{yield}} diff --git a/web/app/components/x/dropdown-list/action.ts b/web/app/components/x/dropdown-list/action.ts new file mode 100644 index 000000000..74d3c4f35 --- /dev/null +++ b/web/app/components/x/dropdown-list/action.ts @@ -0,0 +1,17 @@ +import Component from "@glimmer/component"; + +interface XDropdownListActionComponentSignature { + Element: HTMLButtonElement; + Args: { + registerElement: () => void; + focusMouseTarget: () => void; + onClick: () => void; + disabled?: boolean; + ariaControls: string; + role: string; + isAriaSelected: boolean; + isAriaChecked: boolean; + }; +} + +export default class XDropdownListActionComponent extends Component {} diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index 919d08cff..ee7bb2ac8 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -84,21 +84,22 @@ class="x-dropdown-list-scroll-container" > <:item as |i|> {{yield i to="item"}} diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index 2de63d036..b66486aab 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,28 +1,27 @@
  • {{yield (hash + value=@value + attrs=@attributes + selected=@selected Action=(component "x/dropdown-list/action" - role=@role + role=@listItemRole isAriaSelected=this.isAriaSelected - selected=@selected + isAriaChecked=@selected registerElement=this.registerElement focusMouseTarget=this.focusMouseTarget onClick=this.onClick ) LinkTo=(component "x/dropdown-list/link-to" - role=@role + role=@listItemRole isAriaSelected=this.isAriaSelected - selected=@selected + isAriaChecked=@selected registerElement=this.registerElement focusMouseTarget=this.focusMouseTarget onClick=this.onClick ) - selected=@selected - value=@value - count=@count - attrs=@attributes ) }}
  • diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index 9783cd1d1..b4596d15c 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -9,11 +9,9 @@ import { next } from "@ember/runloop"; interface XDropdownListItemComponentSignature { Args: { - role: string; - selected: boolean; - attributes?: unknown; value: string; - count?: number; + attributes?: unknown; + selected: boolean; focusedItemIndex: number; listItemRole: string; hideDropdown: () => void; diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index 0b36a2aa0..1e5474a1a 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -2,17 +2,13 @@ {{on-document "keydown" this.maybeKeyboardNavigate}} {{#if this.noMatchesFound}} - {{#if (has-block "empty-state")}} - {{yield to="empty-state"}} - {{else}} -
    - No matches -
    - {{/if}} +
    + No matches +
    {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} {{yield i to="item"}} diff --git a/web/app/components/x/dropdown-list/items.ts b/web/app/components/x/dropdown-list/items.ts index 7f2bd1115..a7a593ec2 100644 --- a/web/app/components/x/dropdown-list/items.ts +++ b/web/app/components/x/dropdown-list/items.ts @@ -3,41 +3,53 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { FocusDirection } from "."; -interface XDropdownListItemsComponentSignature { +interface XDropdownListItemsComponentSignature { Args: { - id: string; + contentID: string; + query?: string; items?: any; - shownItems: any; - selected: any; + shownItems?: any; + selected?: any; focusedItemIndex: number; inputIsShown?: boolean; - inputPlaceholder?: string; - isOrdered?: boolean; - onChange: (e: Event) => void; + listIsOrdered?: boolean; + listItemRole: string; + scrollContainer: HTMLElement; + onInput: () => void; + onItemClick: () => void; resetFocusedItemIndex: () => void; registerScrollContainer?: (e: HTMLElement) => void; setFocusedItemIndex: (direction: FocusDirection) => void; - content: HTMLElement | null; hideContent: () => void; }; } -export default class XDropdownListItemsComponent extends Component< - XDropdownListItemsComponentSignature -> { +export default class XDropdownListItemsComponent extends Component { + /** + * The `aria-activedescendant` attribute of the list. + * Used to indicate which item is currently focused. + */ get ariaActiveDescendant() { if (this.args.focusedItemIndex !== -1) { return `x-dropdown-list-item-${this.args.focusedItemIndex}`; } } + /** + * Whether the "no matches found" message should be shown. + * True if the input is shown and there are no items to show. + */ protected get noMatchesFound(): boolean { if (!this.args.inputIsShown) { return false; } return Object.entries(this.args.shownItems).length === 0; } - + /** + * Keyboard listener for the ArrowUp/ArrowDown/Enter keys. + * ArrowUp/ArrowDown change the focused item. + * Enter selects the focused item. + */ @action protected maybeKeyboardNavigate(event: KeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); @@ -51,8 +63,8 @@ export default class XDropdownListItemsComponent extends Component< if (event.key === "Enter") { event.preventDefault(); - assert("floatingUI content must exist", this.args.content); - const target = this.args.content.querySelector("[aria-selected]"); + assert("floatingUI content must exist", this.args.scrollContainer); + const target = this.args.scrollContainer.querySelector("[aria-selected]"); if ( target instanceof HTMLAnchorElement || diff --git a/web/app/components/x/dropdown-list/link-to.hbs b/web/app/components/x/dropdown-list/link-to.hbs index 660dd9fe3..6b55b9c4e 100644 --- a/web/app/components/x/dropdown-list/link-to.hbs +++ b/web/app/components/x/dropdown-list/link-to.hbs @@ -5,11 +5,10 @@ role={{@role}} aria-selected={{@isAriaSelected}} tabindex="-1" - aria-checked={{@selected}} + aria-checked={{@isAriaChecked}} @route={{@route}} @query={{or @query (hash)}} - class="x-dropdown-list-item-link - {{if @isAriaSelected 'is-aria-selected'}}" + class="x-dropdown-list-item-link {{if @isAriaSelected 'is-aria-selected'}}" ...attributes > {{yield}} diff --git a/web/app/components/x/dropdown-list/link-to.ts b/web/app/components/x/dropdown-list/link-to.ts new file mode 100644 index 000000000..5705ba868 --- /dev/null +++ b/web/app/components/x/dropdown-list/link-to.ts @@ -0,0 +1,19 @@ +import Component from "@glimmer/component"; + +interface XDropdownListLinkToComponentSignature { + Element: HTMLButtonElement; + Args: { + registerElement: () => void; + focusMouseTarget: () => void; + onClick: () => void; + disabled?: boolean; + ariaControls: string; + role: string; + isAriaSelected: boolean; + isAriaChecked: boolean; + route: string; + query?: unknown; + }; +} + +export default class XDropdownListLinkToComponent extends Component {} diff --git a/web/app/components/x/dropdown-list/toggle-action.ts b/web/app/components/x/dropdown-list/toggle-action.ts new file mode 100644 index 000000000..a340ccd77 --- /dev/null +++ b/web/app/components/x/dropdown-list/toggle-action.ts @@ -0,0 +1,14 @@ +import Component from "@glimmer/component"; + +interface XDropdownListToggleActionComponentSignature { + Element: HTMLButtonElement; + Args: { + registerAnchor: () => void; + onTriggerKeydown: () => void; + toggleContent: () => void; + disabled?: boolean; + ariaControls: string; + }; +} + +export default class XDropdownListToggleActionComponent extends Component {} diff --git a/web/app/router.js b/web/app/router.js index a4eb30c50..9c7f6cdd1 100644 --- a/web/app/router.js +++ b/web/app/router.js @@ -20,5 +20,5 @@ Router.map(function () { }); }); this.route("authenticate"); - this.route("404", { path: "/*path" }); + this.route('404', { path: '/*path' }) }); From 7a86f942af91cb5ee044364697d5a309e196ac95 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 09:34:11 -0400 Subject: [PATCH 047/128] Clean up FacetDropdown --- web/app/components/header/facet-dropdown.ts | 303 ------------------ .../components/x/dropdown-list/index-test.ts | 0 2 files changed, 303 deletions(-) create mode 100644 web/tests/integration/components/x/dropdown-list/index-test.ts diff --git a/web/app/components/header/facet-dropdown.ts b/web/app/components/header/facet-dropdown.ts index 3f057fd0e..6cc61043f 100644 --- a/web/app/components/header/facet-dropdown.ts +++ b/web/app/components/header/facet-dropdown.ts @@ -1,11 +1,5 @@ import Component from "@glimmer/component"; -import { action } from "@ember/object"; import { FacetDropdownObjects } from "hermes/types/facets"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { restartableTask } from "ember-concurrency"; -import { schedule } from "@ember/runloop"; -import { FocusDirection } from "../x/dropdown-list"; import { inject as service } from "@ember/service"; import RouterService from "@ember/routing/router-service"; @@ -20,304 +14,7 @@ interface FacetDropdownComponentSignature { export default class FacetDropdownComponent extends Component { @service declare router: RouterService; - @tracked private _triggerElement: HTMLButtonElement | null = null; - @tracked private _scrollContainer: HTMLElement | null = null; - @tracked private _popoverElement: HTMLDivElement | null = null; - - @tracked protected query: string = ""; - @tracked protected listItemRole = this.inputIsShown ? "option" : "menuitem"; - @tracked protected dropdownIsShown = false; - @tracked protected focusedItemIndex = -1; - @tracked protected _shownFacets: FacetDropdownObjects | null = null; - - /** - * The dropdown menu items. Registered on insert and - * updated with on keydown and filterInput events. - * Used to determine the list length, and to find the focused - * element by index. - */ - @tracked protected menuItems: NodeListOf | null = null; - - /** - * An asserted-true reference to the scroll container. - * Used in the `maybeScrollIntoView` calculations. - */ - private get scrollContainer(): HTMLElement { - assert("_scrollContainer must exist", this._scrollContainer); - return this._scrollContainer; - } - protected get currentRouteName() { return this.router.currentRouteName; } - - /** - * An asserted-true reference to the popover div. - * Used to scope querySelectorAll calls. - */ - private get popoverElement(): HTMLDivElement { - assert("_popoverElement must exist", this._popoverElement); - return this._popoverElement; - } - - /** - * The dropdown trigger. - * Passed to the dismissible modifier as a dropdown relative. - */ - protected get triggerElement(): HTMLButtonElement { - assert("_triggerElement must exist", this._triggerElement); - return this._triggerElement; - } - - /** - * The facets that should be shown in the dropdown. - * Initially the same as the facets passed in and - * updated when the user types in the filter input. - */ - protected get shownFacets(): FacetDropdownObjects { - if (this._shownFacets) { - return this._shownFacets; - } else { - return this.args.facets; - } - } - - /** - * Whether the filter input should be shown. - * True when the input has more facets than - * can be shown in the dropdown (12). - */ - protected get inputIsShown() { - return Object.entries(this.args.facets).length > 12; - } - - @action protected registerTrigger(element: HTMLButtonElement) { - this._triggerElement = element; - } - - @action protected registerPopover(element: HTMLDivElement) { - this._popoverElement = element; - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - } - - @action protected registerScrollContainer(element: HTMLDivElement) { - this._scrollContainer = element; - } - - /** - * The action run when the popover is inserted, and when - * the user filters or navigates the dropdown. - * Loops through the menu items and assigns an id that - * matches the index of the item in the list. - */ - @action assignMenuItemIDs(items: NodeListOf): void { - this.menuItems = items; - for (let i = 0; i < items.length; i++) { - let item = items[i]; - assert("item must exist", item instanceof HTMLElement); - item.id = `facet-dropdown-menu-item-${i}`; - } - } - - /** - * The action run when the user presses a key. - * Handles the arrow keys to navigate the dropdown. - */ - @action protected onKeydown(event: KeyboardEvent) { - if (event.key === "ArrowDown") { - event.preventDefault(); - this.setFocusedItemIndex(FocusDirection.Next); - } - if (event.key === "ArrowUp") { - event.preventDefault(); - this.setFocusedItemIndex(FocusDirection.Previous); - } - } - - /** - * Toggles the dropdown visibility. - * Called when the user clicks on the dropdown trigger. - */ - @action protected toggleDropdown(): void { - if (this.dropdownIsShown) { - this.hideDropdown(); - } else { - this.showDropdown(); - } - } - - @action protected showDropdown(): void { - this.dropdownIsShown = true; - schedule("afterRender", () => { - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); - } - - /** - * The action run when the user clicks outside the dropdown. - * Hides the dropdown and resets the various tracked states. - */ - @action protected hideDropdown(): void { - this.query = ""; - this.dropdownIsShown = false; - this._shownFacets = null; - this.resetFocusedItemIndex(); - } - - /** - * The action run when the trigger is focused and the user - * presses the up or down arrow keys. Used to open and focus - * to the first or last item in the dropdown. - */ - @action protected onTriggerKeydown(event: KeyboardEvent) { - if (this.dropdownIsShown) { - return; - } - - if (event.key === "ArrowUp" || event.key === "ArrowDown") { - event.preventDefault(); - this.showDropdown(); - - // Stop the event from bubbling to the popover's keydown handler. - event.stopPropagation(); - - // Wait for the menuItems to be set by the showDropdown action. - schedule("afterRender", () => { - switch (event.key) { - case "ArrowDown": - this.setFocusedItemIndex(FocusDirection.First, false); - break; - case "ArrowUp": - this.setFocusedItemIndex(FocusDirection.Last); - break; - } - }); - } - } - - /** - * Sets the focus to the next or previous menu item. - * Used by the onKeydown action to navigate the dropdown, and - * by the FacetDropdownListItem component on mouseenter.s - */ - @action protected setFocusedItemIndex( - focusDirectionOrNumber: FocusDirection | number, - maybeScrollIntoView = true - ) { - let { menuItems, focusedItemIndex } = this; - - let setFirst = () => { - focusedItemIndex = 0; - }; - - let setLast = () => { - assert("menuItems must exist", menuItems); - focusedItemIndex = menuItems.length - 1; - }; - - if (!menuItems) { - return; - } - - if (menuItems.length === 0) { - return; - } - - switch (focusDirectionOrNumber) { - case FocusDirection.Previous: - if (focusedItemIndex === -1 || focusedItemIndex === 0) { - // When the first or no item is focused, "previous" focuses the last item. - setLast(); - } else { - focusedItemIndex--; - } - break; - case FocusDirection.Next: - if (focusedItemIndex === menuItems.length - 1) { - // When the last item is focused, "next" focuses the first item. - setFirst(); - } else { - focusedItemIndex++; - } - break; - case FocusDirection.First: - setFirst(); - break; - case FocusDirection.Last: - setLast(); - break; - default: - focusedItemIndex = focusDirectionOrNumber; - break; - } - - this.focusedItemIndex = focusedItemIndex; - - if (maybeScrollIntoView) { - this.maybeScrollIntoView(); - } - } - - /** - * Checks whether the focused item is completely visible, - * and, if necessary, scrolls the dropdown to make it visible. - * Used by the setFocusedItemIndex action on keydown. - */ - private maybeScrollIntoView() { - const focusedItem = this.menuItems?.item(this.focusedItemIndex); - assert("focusedItem must exist", focusedItem instanceof HTMLElement); - - const containerTopPadding = 12; - const containerHeight = this.scrollContainer.offsetHeight; - const itemHeight = focusedItem.offsetHeight; - const itemTop = focusedItem.offsetTop; - const itemBottom = focusedItem.offsetTop + itemHeight; - const scrollviewTop = this.scrollContainer.scrollTop - containerTopPadding; - const scrollviewBottom = scrollviewTop + containerHeight; - - if (itemBottom > scrollviewBottom) { - this.scrollContainer.scrollTop = itemTop + itemHeight - containerHeight; - } else if (itemTop < scrollviewTop) { - this.scrollContainer.scrollTop = itemTop; - } - } - - /** - * Resets the focus index to its initial value. - * Called when the dropdown is closed, and when the input is focused. - */ - @action protected resetFocusedItemIndex() { - this.focusedItemIndex = -1; - } - - /** - * The action run when the user types in the input. - * Filters the facets shown in the dropdown and schedules - * the menu items to be assigned their new IDs. - */ - protected onInput = restartableTask(async (inputEvent: InputEvent) => { - this.focusedItemIndex = -1; - - let shownFacets: FacetDropdownObjects = {}; - let facets = this.args.facets; - - this.query = (inputEvent.target as HTMLInputElement).value; - for (const [key, value] of Object.entries(facets)) { - if (key.toLowerCase().includes(this.query.toLowerCase())) { - shownFacets[key] = value; - } - } - - this._shownFacets = shownFacets; - - schedule("afterRender", () => { - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); - }); } diff --git a/web/tests/integration/components/x/dropdown-list/index-test.ts b/web/tests/integration/components/x/dropdown-list/index-test.ts new file mode 100644 index 000000000..e69de29bb From 36e4031e751b5705895fdb634777c7eb7cefd501 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 10:46:01 -0400 Subject: [PATCH 048/128] Move FacetDropdown tests to DropdownListIndex --- web/app/components/x/dropdown-list/index.hbs | 3 + web/app/components/x/dropdown-list/index.ts | 28 ++-- web/app/components/x/dropdown-list/item.hbs | 2 +- web/app/components/x/dropdown-list/items.hbs | 3 +- .../components/x/dropdown-list/index-test.ts | 135 ++++++++++++++++++ 5 files changed, 157 insertions(+), 14 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index ee7bb2ac8..824f2bf5b 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -10,6 +10,8 @@ @renderOut={{true}} @placement={{@placement}} class="hermes-popover" + data-test-x-dropdown-list-content + {{will-destroy this.onDestroy}} > <:anchor as |f|> {{yield @@ -65,6 +67,7 @@ {{#if this.inputIsShown}}
    { - assert( - "didInsertContent expects a _scrollContainer", - this._scrollContainer - ); - this.assignMenuItemIDs( - this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); + assert( + "didInsertContent expects a _scrollContainer", + this._scrollContainer + ); + this.assignMenuItemIDs( + this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) + ); + } + + @action onDestroy() { + this.query = ""; + this._filteredItems = null; + this.resetFocusedItemIndex(); } /** @@ -141,7 +145,7 @@ export default class XDropdownListComponent extends Component< for (let i = 0; i < items.length; i++) { let item = items[i]; assert("item must exist", item instanceof HTMLElement); - item.id = `facet-dropdown-menu-item-${i}`; + item.id = `x-dropdown-list-item-${i}`; } } @@ -163,8 +167,8 @@ export default class XDropdownListComponent extends Component< event.preventDefault(); showContent(); - // Wait for the menuItems to be set by the showContent action. - next(() => { + // Wait for menuItemIDs to be set by `didInsertContent`. + schedule("afterRender", () => { switch (event.key) { case "ArrowDown": this.setFocusedItemIndex(FocusDirection.First, false); diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index b66486aab..0eeb0f728 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,4 +1,4 @@ -
  • +
  • {{yield (hash value=@value diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index 1e5474a1a..16b352f86 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -2,12 +2,13 @@ {{on-document "keydown" this.maybeKeyboardNavigate}} {{#if this.noMatchesFound}} -
    +
    No matches
    {{else}} {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert + .dom("[data-test-x-dropdown-list-input]") + .doesNotExist("The input is not shown"); + + this.set("items", LONG_ITEM_LIST); + + assert + .dom("[data-test-x-dropdown-list-input]") + .exists("The input is shown"); + }); + + test("filtering works as expected", async function (assert) { + this.set("items", LONG_ITEM_LIST); + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert.dom("#" + FIRST_ITEM_SELECTOR).hasText("Filter01"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 8 }); + + await fillIn("[data-test-x-dropdown-list-input]", "2"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 1 }); + + assert + .dom("#" + FIRST_ITEM_SELECTOR) + .hasText("Filter02", "the list is filtered and the IDs are updated"); + + await fillIn("[data-test-x-dropdown-list-input]", "foobar"); + + assert.dom("[data-test-x-dropdown-list]").doesNotExist(); + assert.dom("[data-test-dropdown-list-empty-state]").hasText("No matches"); + }); + + test("dropdown trigger has keyboard support", async function (assert) { + this.set("facets", LONG_ITEM_LIST); + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + assert + .dom("[data-test-x-dropdown-list-content]") + .doesNotExist("The popover is not shown"); + + await triggerKeyEvent("[data-test-toggle]", "keydown", "ArrowDown"); + + assert + .dom("[data-test-x-dropdown-list-content]") + .exists("The popover is shown"); + + await waitFor(".is-aria-selected"); + + assert + .dom("#" + FIRST_ITEM_SELECTOR) + .hasClass("is-aria-selected", "the aria-selected class is applied") + .hasAttribute("aria-selected"); + + assert + .dom("[data-test-x-dropdown-list]") + .hasAttribute("aria-activedescendant", FIRST_ITEM_SELECTOR); + }); +}); From 618a4bb57acbaab9c2e474f84efc6988ac7ecb5b Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 11:33:46 -0400 Subject: [PATCH 049/128] Add aria test --- web/app/components/x/dropdown-list/index.hbs | 6 +- web/app/components/x/dropdown-list/index.ts | 24 +---- web/app/components/x/dropdown-list/items.hbs | 2 +- web/app/components/x/dropdown-list/items.ts | 1 - .../components/x/dropdown-list/index-test.ts | 91 ++++++++++++++++++- 5 files changed, 93 insertions(+), 31 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index 824f2bf5b..7ee9fa488 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -28,7 +28,7 @@ disabled=@disabled ariaControls=(concat "x-dropdown-list-" - (if this.inputIsShown "container" "list") + (if this.inputIsShown "container" "items") "-" f.contentID ) @@ -44,7 +44,7 @@ disabled=@disabled ariaControls=(concat "x-dropdown-list-" - (if this.inputIsShown "container" "list") + (if this.inputIsShown "container" "items") "-" f.contentID ) @@ -70,7 +70,6 @@ data-test-x-dropdown-list-input {{did-insert this.registerAndFocusInput}} {{on "input" (perform this.onInput)}} - {{on "focusin" this.resetFocusedItemIndex}} @value={{this.query}} @type="search" placeholder="Filter..." @@ -98,7 +97,6 @@ @listItemRole={{this.listItemRole}} @onInput={{perform this.onInput}} @onItemClick={{@onItemClick}} - @resetFocusedItemIndex={{this.resetFocusedItemIndex}} @registerScrollContainer={{this.registerScrollContainer}} @setFocusedItemIndex={{this.setFocusedItemIndex}} @hideContent={{f.hideContent}} diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 8120493ee..a201e5437 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -75,19 +75,6 @@ export default class XDropdownListComponent extends Component< return this._filteredItems || this.args.items; } - /** - * The "aria-controls" value for the dropdown trigger. - */ - get ariaControls() { - let value = "x-dropdown-"; - if (this.inputIsShown) { - value += "popover"; - } else { - value += "list"; - } - return `${value}-`; - } - /** * The action run when the scrollContainer is inserted. * Registers the div for reference locally. @@ -122,7 +109,7 @@ export default class XDropdownListComponent extends Component< @action onDestroy() { this.query = ""; this._filteredItems = null; - this.resetFocusedItemIndex(); + this.focusedItemIndex = -1; } /** @@ -267,15 +254,6 @@ export default class XDropdownListComponent extends Component< this.scrollContainer.scrollTop = itemTop; } } - - /** - * Resets the focus index to its initial value. - * Called when the dropdown is closed, and when the input is focused. - */ - @action protected resetFocusedItemIndex() { - this.focusedItemIndex = -1; - } - /** * The action run when the user types in the input. * Filters the facets shown in the dropdown and schedules diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index 16b352f86..c00409a50 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -9,7 +9,7 @@ {{#let (element (if @listIsOrdered "ol" "ul")) as |MaybeOrderedList|}} void; onItemClick: () => void; - resetFocusedItemIndex: () => void; registerScrollContainer?: (e: HTMLElement) => void; setFocusedItemIndex: (direction: FocusDirection) => void; hideContent: () => void; 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 32dc886f1..6b7bb8785 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -3,6 +3,8 @@ import { setupRenderingTest } from "ember-qunit"; import { click, fillIn, + find, + findAll, render, triggerKeyEvent, waitFor, @@ -38,7 +40,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await render(hbs` <:anchor as |dd|> - + <:item as |dd|> @@ -48,7 +50,15 @@ module("Integration | Component | x/dropdown-list", function (hooks) { `); - await click("button"); + let ariaControlsValue = + find("[data-test-toggle]")?.getAttribute("aria-controls"); + + assert.ok( + ariaControlsValue?.startsWith("x-dropdown-list-items"), + "the correct aria-controls attribute is set" + ); + + await click("[data-test-toggle]"); assert .dom("[data-test-x-dropdown-list-input]") @@ -59,6 +69,20 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert .dom("[data-test-x-dropdown-list-input]") .exists("The input is shown"); + + ariaControlsValue = + find("[data-test-toggle]")?.getAttribute("aria-controls"); + + assert.ok( + ariaControlsValue?.startsWith("x-dropdown-list-container"), + "the correct aria-controls attribute is set" + ); + + assert.equal( + document.activeElement, + this.element.querySelector("[data-test-x-dropdown-list-input]"), + "the input is autofocused" + ); }); test("filtering works as expected", async function (assert) { @@ -132,4 +156,67 @@ module("Integration | Component | x/dropdown-list", function (hooks) { .dom("[data-test-x-dropdown-list]") .hasAttribute("aria-activedescendant", FIRST_ITEM_SELECTOR); }); + + test("the component's filter properties are reset on close", async function (assert) { + this.set("facets", LONG_ITEM_LIST); + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 8 }); + assert.dom("[data-test-x-dropdown-list-input]").hasValue(""); + + await fillIn("[data-test-x-dropdown-list-input]", "2"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 1 }); + assert.dom("[data-test-x-dropdown-list-input]").hasValue("2"); + + // close and reopen + await click("button"); + await click("button"); + + assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 8 }); + assert.dom("[data-test-x-dropdown-list-input]").hasValue(""); + }); + + test("the menu items are assigned IDs", async function (assert) { + this.set("facets", LONG_ITEM_LIST); + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + const listItemIDs = findAll("[data-test-item-button]").map((item) => { + // the item's full id is "x-dropdown-list-item-0" + // but we only need the number + return item.id.split("-").pop(); + }); + + assert.deepEqual( + listItemIDs, + ["0", "1", "2", "3", "4", "5", "6", "7"], + "the IDs are assigned in order" + ); + }); }); From 68b9aa80a87b74ce4093a9a1ff0e51294782088c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 11:46:03 -0400 Subject: [PATCH 050/128] Test Keyboard navigation --- web/app/components/x/dropdown-list/index.ts | 2 +- .../components/x/dropdown-list/index-test.ts | 107 ++++++++++++++++-- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index a201e5437..c0ce3c85c 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -1,6 +1,6 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; -import { next, schedule } from "@ember/runloop"; +import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; 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 6b7bb8785..bc3300b51 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -30,6 +30,8 @@ export const LONG_ITEM_LIST = { }; const FIRST_ITEM_SELECTOR = "x-dropdown-list-item-0"; +const SECOND_ITEM_SELECTOR = "x-dropdown-list-item-1"; +const LAST_ITEM_SELECTOR = "x-dropdown-list-item-7"; module("Integration | Component | x/dropdown-list", function (hooks) { setupRenderingTest(hooks); @@ -121,9 +123,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { }); test("dropdown trigger has keyboard support", async function (assert) { - this.set("facets", LONG_ITEM_LIST); + this.set("items", LONG_ITEM_LIST); await render(hbs` - + <:anchor as |dd|> @@ -158,9 +160,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { }); test("the component's filter properties are reset on close", async function (assert) { - this.set("facets", LONG_ITEM_LIST); + this.set("items", LONG_ITEM_LIST); await render(hbs` - + <:anchor as |dd|> @@ -191,9 +193,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { }); test("the menu items are assigned IDs", async function (assert) { - this.set("facets", LONG_ITEM_LIST); + this.set("items", LONG_ITEM_LIST); await render(hbs` - + <:anchor as |dd|> @@ -208,8 +210,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await click("button"); const listItemIDs = findAll("[data-test-item-button]").map((item) => { - // the item's full id is "x-dropdown-list-item-0" - // but we only need the number + // grab the number from the item's ID (`x-dropdown-list-item-0`) return item.id.split("-").pop(); }); @@ -219,4 +220,94 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "the IDs are assigned in order" ); }); + + test("the list has keyboard support", async function (assert) { + this.set("items", LONG_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert.false( + findAll("[data-test-item-button]").some( + (item) => item.getAttribute("aria-selected") === "true" + ), + "no items are aria-selected" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert + .dom("#" + FIRST_ITEM_SELECTOR) + .hasAttribute("aria-selected", "true", "the first item is aria-selected"); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + + assert + .dom("#" + SECOND_ITEM_SELECTOR) + .hasAttribute( + "aria-selected", + "true", + "the second item is aria-selected" + ); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); + + assert + .dom("#" + SECOND_ITEM_SELECTOR) + .doesNotHaveAttribute("aria-selected"); + + assert + .dom("#" + FIRST_ITEM_SELECTOR) + .hasAttribute("aria-selected", "true", "the first item is aria-selected"); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); + + assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + + assert + .dom("#" + LAST_ITEM_SELECTOR) + .hasAttribute( + "aria-selected", + "true", + "the last item is aria-selected when pressing up from the first" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert.dom("#" + LAST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + + assert + .dom("#" + FIRST_ITEM_SELECTOR) + .hasAttribute( + "aria-selected", + "true", + "the first item is aria-selected when pressing down from the last" + ); + }); }); From 0ab15807ff46c976f08ebf05e9381a8e0eec9a3f Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 11:48:48 -0400 Subject: [PATCH 051/128] Test Enter key --- .../components/x/dropdown-list/index-test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 bc3300b51..5faecf834 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -223,6 +223,10 @@ module("Integration | Component | x/dropdown-list", function (hooks) { test("the list has keyboard support", async function (assert) { this.set("items", LONG_ITEM_LIST); + this.set("buttonWasClicked", false); + this.set("onButtonClick", () => { + this.set("buttonWasClicked", true); + }); await render(hbs` @@ -230,11 +234,15 @@ module("Integration | Component | x/dropdown-list", function (hooks) { <:item as |dd|> - + {{dd.value}} + + {{#if this.buttonWasClicked}} +
    Button was clicked
    + {{/if}} `); await click("button"); @@ -309,5 +317,13 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "true", "the first item is aria-selected when pressing down from the last" ); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "Enter"); + + assert + .dom("[data-test-button-clicked]") + .exists( + "keying Enter triggers the click action of the aria-selected item" + ); }); }); From 1068f0bd9f8ea06e13a19b43e3295618b7160fcf Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 15:23:32 -0400 Subject: [PATCH 052/128] Add keyboard tests --- web/app/components/x/dropdown-list/index.ts | 3 +- web/app/components/x/dropdown-list/item.hbs | 6 +- web/app/components/x/dropdown-list/item.ts | 8 +- .../components/x/dropdown/list-item.scss | 2 +- .../components/x/dropdown-list/index-test.ts | 233 +++++++++++++++--- 5 files changed, 204 insertions(+), 48 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index c0ce3c85c..307539921 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -240,12 +240,11 @@ export default class XDropdownListComponent extends Component< const focusedItem = this._menuItems?.item(this.focusedItemIndex); assert("focusedItem must exist", focusedItem instanceof HTMLElement); - const containerTopPadding = 12; const containerHeight = this.scrollContainer.offsetHeight; const itemHeight = focusedItem.offsetHeight; const itemTop = focusedItem.offsetTop; const itemBottom = focusedItem.offsetTop + itemHeight; - const scrollviewTop = this.scrollContainer.scrollTop - containerTopPadding; + const scrollviewTop = this.scrollContainer.scrollTop; const scrollviewBottom = scrollviewTop + containerHeight; if (itemBottom > scrollviewBottom) { diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index 0eeb0f728..208506283 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,9 +1,6 @@
  • {{yield (hash - value=@value - attrs=@attributes - selected=@selected Action=(component "x/dropdown-list/action" role=@listItemRole @@ -22,6 +19,9 @@ focusMouseTarget=this.focusMouseTarget onClick=this.onClick ) + value=@value + attrs=@attributes + selected=@selected ) }}
  • diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index b4596d15c..da3606e6f 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -55,13 +55,11 @@ export default class XDropdownListItemComponent extends Component { + this.set("onListItemClick", () => { this.set("buttonWasClicked", true); }); @@ -234,7 +238,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { <:item as |dd|> - + {{dd.value}} @@ -248,8 +252,8 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await click("button"); assert.false( - findAll("[data-test-item-button]").some( - (item) => item.getAttribute("aria-selected") === "true" + findAll("[data-test-item-button]").some((item) => + item.getAttribute("aria-selected") ), "no items are aria-selected" ); @@ -260,9 +264,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - assert - .dom("#" + FIRST_ITEM_SELECTOR) - .hasAttribute("aria-selected", "true", "the first item is aria-selected"); + assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); await triggerKeyEvent( "[data-test-x-dropdown-list]", @@ -271,36 +273,19 @@ module("Integration | Component | x/dropdown-list", function (hooks) { ); assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); - - assert - .dom("#" + SECOND_ITEM_SELECTOR) - .hasAttribute( - "aria-selected", - "true", - "the second item is aria-selected" - ); + assert.dom("#" + SECOND_ITEM_SELECTOR).hasAttribute("aria-selected"); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); assert .dom("#" + SECOND_ITEM_SELECTOR) .doesNotHaveAttribute("aria-selected"); - - assert - .dom("#" + FIRST_ITEM_SELECTOR) - .hasAttribute("aria-selected", "true", "the first item is aria-selected"); + assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); - - assert - .dom("#" + LAST_ITEM_SELECTOR) - .hasAttribute( - "aria-selected", - "true", - "the last item is aria-selected when pressing up from the first" - ); + assert.dom("#" + LAST_ITEM_SELECTOR).hasAttribute("aria-selected"); await triggerKeyEvent( "[data-test-x-dropdown-list]", @@ -310,20 +295,194 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom("#" + LAST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert - .dom("#" + FIRST_ITEM_SELECTOR) - .hasAttribute( - "aria-selected", - "true", - "the first item is aria-selected when pressing down from the last" - ); + .dom("[data-test-button-clicked]") + .doesNotExist("the button has not been clicked yet"); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "Enter"); - assert .dom("[data-test-button-clicked]") .exists( "keying Enter triggers the click action of the aria-selected item" ); + + assert + .dom("[data-test-x-dropdown-list]") + .doesNotExist("the dropdown list is closed when Enter is pressed"); + }); + + test("the list responds to hover events", async function (assert) { + this.set("items", LONG_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert.false( + findAll("[data-test-item-button]").some((item) => + item.getAttribute("aria-selected") + ), + "no items are aria-selected" + ); + + await triggerEvent("#" + FIRST_ITEM_SELECTOR, "mouseenter"); + + assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + + await triggerEvent("#" + SECOND_ITEM_SELECTOR, "mouseenter"); + + assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + SECOND_ITEM_SELECTOR).hasAttribute("aria-selected"); + }); + + test("the list will scroll to the selected item when it is not visible", async function (assert) { + this.set("items", LONG_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + // At 160px tall, the fourth item is cropped. + let container = htmlElement(".x-dropdown-list-scroll-container"); + let item = htmlElement("#x-dropdown-list-item-3"); + + const containerHeight = container.offsetHeight; + const itemHeight = item.offsetHeight; + + let itemTop = 0; + let itemBottom = 0; + let scrollviewTop = 0; + let scrollviewBottom = 0; + + function updateMeasurements(selector?: string) { + if (selector) { + item = htmlElement(selector); + } + itemTop = item.offsetTop; + itemBottom = itemTop + itemHeight; + scrollviewTop = container.scrollTop; + scrollviewBottom = scrollviewTop + containerHeight; + } + + updateMeasurements(); + + assert.true( + itemBottom > scrollviewBottom, + "item four is not fully visible" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert.equal( + itemBottom, + item.offsetTop + itemHeight, + "container isn't scrolled unless the target is out of view" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert.equal( + itemBottom, + item.offsetTop + itemHeight, + "container isn't scrolled unless the target is out of view" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + assert.equal( + itemBottom, + item.offsetTop + itemHeight, + "container isn't scrolled unless the target is out of view" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + updateMeasurements(); + + assert.equal( + container.scrollTop, + itemTop + itemHeight - containerHeight, + "item four scrolled into view" + ); + + await triggerKeyEvent( + "[data-test-x-dropdown-list]", + "keydown", + "ArrowDown" + ); + + updateMeasurements('#x-dropdown-list-item-4'); + + assert.equal( + container.scrollTop, + itemTop + itemHeight - containerHeight, + "item five scrolled into view" + ); + + updateMeasurements('#' + SECOND_ITEM_SELECTOR); + + assert.ok(itemBottom > scrollviewTop, "item two is not fully visible"); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); + + assert.equal( + itemTop, + item.offsetTop, + "container isn't scrolled unless the target is out of view" + ); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); + + assert.equal( + itemTop, + item.offsetTop, + "container isn't scrolled unless the target is out of view" + ); + + await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); + + updateMeasurements(); + + assert.equal(scrollviewTop, itemTop, "item two scrolled into view"); }); }); From 40193cf19ff621ddd8c375471a8cc31223020883 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 16:21:09 -0400 Subject: [PATCH 053/128] ToggleButton test --- web/app/components/floating-u-i/content.hbs | 1 + web/app/components/floating-u-i/content.ts | 1 + web/app/components/floating-u-i/index.ts | 1 + web/app/components/x/dropdown-list/index.hbs | 2 +- web/app/components/x/dropdown-list/items.hbs | 2 +- .../x/dropdown-list/toggle-action.hbs | 1 + .../x/dropdown-list/toggle-button.hbs | 2 + .../styles/components/x/dropdown/list.scss | 2 +- .../components/x/dropdown-list/index-test.ts | 114 ++++++++++++++---- 9 files changed, 97 insertions(+), 29 deletions(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index 5327e13fb..664bac566 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -7,6 +7,7 @@ {{will-destroy this.cleanup}} {{did-insert this.didInsert}} data-test-floating-ui-placement={{@placement}} + data-anchored-to={{@id}} id="floating-ui-content-{{@id}}" class="hermes-floating-ui-content" ...attributes diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 62c6d0ee1..cb79314e0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -15,6 +15,7 @@ import { tracked } from "@glimmer/tracking"; interface FloatingUIContentSignature { Args: { anchor: HTMLElement; + id: string; placement?: Placement; renderOut?: boolean; }; diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index aeea89c09..f7eb5cb57 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -26,6 +26,7 @@ export default class FloatingUIComponent extends Component diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index c00409a50..e884156c6 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -10,7 +10,7 @@ diff --git a/web/app/components/x/dropdown-list/toggle-action.hbs b/web/app/components/x/dropdown-list/toggle-action.hbs index c08fc26c7..935f8db9b 100644 --- a/web/app/components/x/dropdown-list/toggle-action.hbs +++ b/web/app/components/x/dropdown-list/toggle-action.hbs @@ -4,6 +4,7 @@ {{on "click" @toggleContent}} disabled={{@disabled}} aria-controls={{@ariaControls}} + aria-expanded={{@contentIsShown}} aria-haspopup="listbox" ...attributes > diff --git a/web/app/components/x/dropdown-list/toggle-button.hbs b/web/app/components/x/dropdown-list/toggle-button.hbs index f90e13b94..d397f73fd 100644 --- a/web/app/components/x/dropdown-list/toggle-button.hbs +++ b/web/app/components/x/dropdown-list/toggle-button.hbs @@ -1,4 +1,5 @@ diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index 5cec8a79d..7b6868047 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -14,6 +14,6 @@ @apply p-12 text-center text-color-foreground-faint; } -.x-dropdown-list { +.x-dropdown-list-items { @apply py-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 84e941c17..7dce44e3e 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -33,9 +33,11 @@ export const LONG_ITEM_LIST = { Filter08: { count: 1, selected: false }, }; -const FIRST_ITEM_SELECTOR = "x-dropdown-list-item-0"; -const SECOND_ITEM_SELECTOR = "x-dropdown-list-item-1"; -const LAST_ITEM_SELECTOR = "x-dropdown-list-item-7"; +const CONTAINER_CLASS = "x-dropdown-list"; +const TOGGLE_BUTTON_SELECTOR = "[data-test-x-dropdown-list-toggle-button]"; +const FIRST_ITEM_ID = "x-dropdown-list-item-0"; +const SECOND_ITEM_ID = "x-dropdown-list-item-1"; +const LAST_ITEM_ID = "x-dropdown-list-item-7"; module("Integration | Component | x/dropdown-list", function (hooks) { setupRenderingTest(hooks); @@ -80,7 +82,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { find("[data-test-toggle]")?.getAttribute("aria-controls"); assert.ok( - ariaControlsValue?.startsWith("x-dropdown-list-container"), + ariaControlsValue?.startsWith(CONTAINER_CLASS), "the correct aria-controls attribute is set" ); @@ -108,7 +110,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await click("button"); - assert.dom("#" + FIRST_ITEM_SELECTOR).hasText("Filter01"); + assert.dom("#" + FIRST_ITEM_ID).hasText("Filter01"); assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 8 }); @@ -117,7 +119,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.dom("[data-test-x-dropdown-list-item]").exists({ count: 1 }); assert - .dom("#" + FIRST_ITEM_SELECTOR) + .dom("#" + FIRST_ITEM_ID) .hasText("Filter02", "the list is filtered and the IDs are updated"); await fillIn("[data-test-x-dropdown-list-input]", "foobar"); @@ -154,13 +156,13 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await waitFor(".is-aria-selected"); assert - .dom("#" + FIRST_ITEM_SELECTOR) + .dom("#" + FIRST_ITEM_ID) .hasClass("is-aria-selected", "the aria-selected class is applied") .hasAttribute("aria-selected"); assert .dom("[data-test-x-dropdown-list]") - .hasAttribute("aria-activedescendant", FIRST_ITEM_SELECTOR); + .hasAttribute("aria-activedescendant", FIRST_ITEM_ID); }); test("the component's filter properties are reset on close", async function (assert) { @@ -264,7 +266,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).hasAttribute("aria-selected"); await triggerKeyEvent( "[data-test-x-dropdown-list]", @@ -272,20 +274,18 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); - assert.dom("#" + SECOND_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + SECOND_ITEM_ID).hasAttribute("aria-selected"); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); - assert - .dom("#" + SECOND_ITEM_SELECTOR) - .doesNotHaveAttribute("aria-selected"); - assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + SECOND_ITEM_ID).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).hasAttribute("aria-selected"); await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); - assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); - assert.dom("#" + LAST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + LAST_ITEM_ID).hasAttribute("aria-selected"); await triggerKeyEvent( "[data-test-x-dropdown-list]", @@ -293,9 +293,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - assert.dom("#" + LAST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + LAST_ITEM_ID).doesNotHaveAttribute("aria-selected"); - assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).hasAttribute("aria-selected"); assert .dom("[data-test-button-clicked]") @@ -338,14 +338,14 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "no items are aria-selected" ); - await triggerEvent("#" + FIRST_ITEM_SELECTOR, "mouseenter"); + await triggerEvent("#" + FIRST_ITEM_ID, "mouseenter"); - assert.dom("#" + FIRST_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).hasAttribute("aria-selected"); - await triggerEvent("#" + SECOND_ITEM_SELECTOR, "mouseenter"); + await triggerEvent("#" + SECOND_ITEM_ID, "mouseenter"); - assert.dom("#" + FIRST_ITEM_SELECTOR).doesNotHaveAttribute("aria-selected"); - assert.dom("#" + SECOND_ITEM_SELECTOR).hasAttribute("aria-selected"); + assert.dom("#" + FIRST_ITEM_ID).doesNotHaveAttribute("aria-selected"); + assert.dom("#" + SECOND_ITEM_ID).hasAttribute("aria-selected"); }); test("the list will scroll to the selected item when it is not visible", async function (assert) { @@ -451,7 +451,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - updateMeasurements('#x-dropdown-list-item-4'); + updateMeasurements("#x-dropdown-list-item-4"); assert.equal( container.scrollTop, @@ -459,7 +459,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "item five scrolled into view" ); - updateMeasurements('#' + SECOND_ITEM_SELECTOR); + updateMeasurements("#" + SECOND_ITEM_ID); assert.ok(itemBottom > scrollviewTop, "item two is not fully visible"); @@ -485,4 +485,66 @@ module("Integration | Component | x/dropdown-list", function (hooks) { assert.equal(scrollviewTop, itemTop, "item two scrolled into view"); }); + + test("the list can be rendered with a toggle button", async function (assert) { + this.set("items", SHORT_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + {{dd.value}} + + + `); + + assert + .dom(TOGGLE_BUTTON_SELECTOR) + .exists() + .hasClass("hds-button", "the toggle button has the HDS style") + .hasAttribute("aria-haspopup", "listbox") + .doesNotHaveAttribute("aria-expanded"); + + assert.dom(CONTAINER_CLASS).doesNotExist(); + assert.dom(".flight-icon-chevron-down").exists(); + assert.dom(".flight-icon-chevron-up").doesNotExist(); + + await click(TOGGLE_BUTTON_SELECTOR); + + assert.dom(TOGGLE_BUTTON_SELECTOR).hasAttribute("aria-expanded"); + + assert.dom("." + CONTAINER_CLASS).exists(); + assert.dom(".flight-icon-chevron-down").doesNotExist(); + assert.dom(".flight-icon-chevron-up").exists(); + + const ariaControlsValue = htmlElement(TOGGLE_BUTTON_SELECTOR).getAttribute( + "aria-controls" + ); + + const dropdownListItemsID = htmlElement(".x-dropdown-list-items").getAttribute("id"); + + debugger + assert.equal( + ariaControlsValue, + dropdownListItemsID, + "the aria-controls value matches the dropdown list ID" + ); + + let dataAnchorID = htmlElement(TOGGLE_BUTTON_SELECTOR).getAttribute( + "data-anchor-id" + ); + + let contentAnchoredTo = htmlElement("." + CONTAINER_CLASS).getAttribute( + "data-anchored-to" + ); + + + assert.equal( + dataAnchorID, + contentAnchoredTo, + "the anchor is properly registered" + ); + }); }); From 4b14e210e3fb54a0bf21ded930be0471245bf45d Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 16:57:31 -0400 Subject: [PATCH 054/128] LinkTo, CheckableItem tests --- .../x/dropdown-list/checkable-item.hbs | 2 + web/app/components/x/dropdown-list/index.hbs | 1 + .../components/x/dropdown-list/link-to.hbs | 3 + .../x/dropdown-list/toggle-action.hbs | 1 + .../x/dropdown-list/checkable-item-test.ts | 35 +++++ .../components/x/dropdown-list/index-test.ts | 120 ++++++++++++++++-- 6 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 web/tests/integration/components/x/dropdown-list/checkable-item-test.ts diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index 82ba5c5c0..8717250cd 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,4 +1,5 @@ @@ -7,6 +8,7 @@
    {{#if @count}} + `); + + assert.dom(CHECK_SELECTOR).hasClass("invisible"); + assert.dom(".x-dropdown-list-item-value").hasText("foo"); + assert.dom(COUNT_SELECTOR).doesNotExist(); + + this.set("selected", true); + + assert.dom(CHECK_SELECTOR).hasClass("visible"); + + this.set("count", 1); + assert.dom(COUNT_SELECTOR).hasText("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 7dce44e3e..0c08266d1 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -5,19 +5,16 @@ import { fillIn, find, findAll, - getSettledState, render, - settled, triggerEvent, triggerKeyEvent, waitFor, - waitUntil, } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import htmlElement from "hermes/utils/html-element"; -import { assert as emberAssert } from "@ember/debug"; // TODO: Replace with Mirage factories + export const SHORT_ITEM_LIST = { Filter01: { count: 1, selected: false }, Filter02: { count: 1, selected: false }, @@ -35,9 +32,11 @@ export const LONG_ITEM_LIST = { const CONTAINER_CLASS = "x-dropdown-list"; const TOGGLE_BUTTON_SELECTOR = "[data-test-x-dropdown-list-toggle-button]"; +const TOGGLE_ACTION_SELECTOR = "[data-test-x-dropdown-list-toggle-action]"; const FIRST_ITEM_ID = "x-dropdown-list-item-0"; const SECOND_ITEM_ID = "x-dropdown-list-item-1"; const LAST_ITEM_ID = "x-dropdown-list-item-7"; +const LINK_TO_SELECTOR = "[data-test-x-dropdown-list-item-link-to]"; module("Integration | Component | x/dropdown-list", function (hooks) { setupRenderingTest(hooks); @@ -216,7 +215,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await click("button"); const listItemIDs = findAll("[data-test-item-button]").map((item) => { - // grab the number from the item's ID (`x-dropdown-list-item-0`) + // grab the number from the item IDs (e.g., `x-dropdown-list-item-0`) return item.id.split("-").pop(); }); @@ -366,7 +365,6 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await click("button"); - // At 160px tall, the fourth item is cropped. let container = htmlElement(".x-dropdown-list-scroll-container"); let item = htmlElement("#x-dropdown-list-item-3"); @@ -378,7 +376,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { let scrollviewTop = 0; let scrollviewBottom = 0; - function updateMeasurements(selector?: string) { + function measure(selector?: string) { if (selector) { item = htmlElement(selector); } @@ -388,7 +386,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { scrollviewBottom = scrollviewTop + containerHeight; } - updateMeasurements(); + measure(); + + // At 160px tall, the fourth item is cropped. assert.true( itemBottom > scrollviewBottom, @@ -437,7 +437,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - updateMeasurements(); + measure(); assert.equal( container.scrollTop, @@ -451,7 +451,7 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "ArrowDown" ); - updateMeasurements("#x-dropdown-list-item-4"); + measure("#x-dropdown-list-item-4"); assert.equal( container.scrollTop, @@ -459,7 +459,9 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "item five scrolled into view" ); - updateMeasurements("#" + SECOND_ITEM_ID); + measure("#" + SECOND_ITEM_ID); + + // At this point the second item is cropped: assert.ok(itemBottom > scrollviewTop, "item two is not fully visible"); @@ -481,11 +483,40 @@ module("Integration | Component | x/dropdown-list", function (hooks) { await triggerKeyEvent("[data-test-x-dropdown-list]", "keydown", "ArrowUp"); - updateMeasurements(); + measure(); assert.equal(scrollviewTop, itemTop, "item two scrolled into view"); }); + test("the list can be rendered with LinkTos", async function (assert) { + this.set("items", SHORT_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + + + <:item as |dd|> + + {{dd.value}} + + + + `); + + await click("button"); + + assert.dom(LINK_TO_SELECTOR).exists({ count: 3 }); + + const firstLink = htmlElement(LINK_TO_SELECTOR); + + assert.equal( + firstLink.getAttribute("href"), + "/all?products=Labs", + "route and query are set" + ); + }); + test("the list can be rendered with a toggle button", async function (assert) { this.set("items", SHORT_ITEM_LIST); @@ -523,9 +554,10 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "aria-controls" ); - const dropdownListItemsID = htmlElement(".x-dropdown-list-items").getAttribute("id"); + const dropdownListItemsID = htmlElement( + ".x-dropdown-list-items" + ).getAttribute("id"); - debugger assert.equal( ariaControlsValue, dropdownListItemsID, @@ -540,6 +572,66 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "data-anchored-to" ); + assert.equal( + dataAnchorID, + contentAnchoredTo, + "the anchor is properly registered" + ); + }); + + test("the list can be rendered with a toggle action", async function (assert) { + this.set("items", SHORT_ITEM_LIST); + + await render(hbs` + + <:anchor as |dd|> + +
    + I can be anything +
    +
    + + <:item as |dd|> + {{dd.value}} + +
    + `); + + assert + .dom(TOGGLE_ACTION_SELECTOR) + .exists() + .hasAttribute("aria-haspopup", "listbox") + .doesNotHaveAttribute("aria-expanded"); + + assert.dom(CONTAINER_CLASS).doesNotExist(); + + await click(TOGGLE_ACTION_SELECTOR); + + assert.dom(TOGGLE_ACTION_SELECTOR).hasAttribute("aria-expanded"); + + assert.dom("." + CONTAINER_CLASS).exists(); + + const ariaControlsValue = htmlElement(TOGGLE_ACTION_SELECTOR).getAttribute( + "aria-controls" + ); + + const dropdownListItemsID = htmlElement( + ".x-dropdown-list-items" + ).getAttribute("id"); + + assert.equal( + ariaControlsValue, + dropdownListItemsID, + "the aria-controls value matches the dropdown list ID" + ); + + let dataAnchorID = htmlElement(TOGGLE_ACTION_SELECTOR).getAttribute( + "data-anchor-id" + ); + + let contentAnchoredTo = htmlElement("." + CONTAINER_CLASS).getAttribute( + "data-anchored-to" + ); assert.equal( dataAnchorID, From 811ef938e4fdb96e096473e97c5a80a7d620d174 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 17:13:53 -0400 Subject: [PATCH 055/128] CSS tweaks --- web/app/components/inputs/product-select.hbs | 2 +- web/app/styles/app.scss | 1 - .../components/header/facet-dropdown.scss | 47 ------------------- .../components/x/dropdown/list-item.scss | 2 +- .../styles/components/x/dropdown/list.scss | 6 ++- web/app/styles/hds-overrides.scss | 2 +- 6 files changed, 8 insertions(+), 52 deletions(-) delete mode 100644 web/app/styles/components/header/facet-dropdown.scss diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index bffee44e0..418879eb1 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -6,7 +6,7 @@ @listIsOrdered={{true}} @onItemClick={{this.onChange}} @selected={{@selected}} - class="max-h-[320px] w-80" + class="max-h-[340px] w-80" > <:anchor as |dd|>
    diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index d5da6f502..36e92c207 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -23,7 +23,6 @@ @use "components/notification"; @use "components/sidebar"; @use "components/hds-badge"; -@use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; @use "hashicorp/product-badge"; diff --git a/web/app/styles/components/header/facet-dropdown.scss b/web/app/styles/components/header/facet-dropdown.scss deleted file mode 100644 index 92b81a799..000000000 --- a/web/app/styles/components/header/facet-dropdown.scss +++ /dev/null @@ -1,47 +0,0 @@ -.facet-dropdown-popover { - @apply absolute -bottom-1 left-0 bg-color-page-primary translate-y-full flex flex-col rounded-md w-full max-h-[400px] pt-0 z-50; - - &.hds-dropdown__content { - @apply min-w-[175px]; - } - - &.anchor-right { - @apply right-0 left-auto; - } - - &.medium:not(.large) { - @apply w-[240px]; - } - - &.large { - @apply w-[320px]; - } -} - -.facet-dropdown-menu-item-link { - @apply no-underline flex items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; - - &.is-aria-selected { - @apply bg-color-foreground-action text-color-foreground-high-contrast outline-none; - - .flight-icon { - @apply text-inherit; - } - } - - .flight-icon { - @apply text-color-foreground-action shrink-0; - - &.check { - @apply mr-2.5; - } - - &.sort-icon { - @apply ml-3 mr-4; - } - } -} - -.facet-dropdown-menu-item-value { - @apply text-color-foreground-strong whitespace-nowrap truncate; -} diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index ed557321b..764b8664a 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -27,7 +27,7 @@ } .x-dropdown-list-item-value { - @apply truncate whitespace-nowrap; + @apply w-full truncate whitespace-nowrap; } .x-dropdown-list-item-count { diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index 7b6868047..41e08046a 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -1,3 +1,7 @@ +.x-dropdown-list { + @apply max-h-[410px]; +} + .x-dropdown-list-container { @apply flex flex-col overflow-hidden; } @@ -15,5 +19,5 @@ } .x-dropdown-list-items { - @apply py-1; + @apply pt-1 pb-1.5; } diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index 365d1a1bf..b64d0ff37 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -23,7 +23,7 @@ @apply mix-blend-multiply; } -.facet-dropdown-menu-item-link { +.x-dropdown-list-item-link { &.is-aria-selected { .hds-badge-count--color-neutral.hds-badge-count--type-filled { @apply bg-white bg-opacity-10 text-color-foreground-high-contrast mix-blend-screen; From 913eae8d4f46d9d3f4cbcd3b326868035353900a Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 24 Apr 2023 20:37:48 -0400 Subject: [PATCH 056/128] Update list.scss --- web/app/styles/components/x/dropdown/list.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index 41e08046a..c5762d751 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -1,5 +1,9 @@ .x-dropdown-list { @apply max-h-[410px]; + + &.hds-dropdown__content { + @apply min-w-[175px]; + } } .x-dropdown-list-container { From 8a079227f3177ee249744a8f73fffda960729909 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 10:55:29 -0400 Subject: [PATCH 057/128] Add ToggleSelect and update NewDocForm --- web/app/components/document/sidebar.hbs | 2 +- .../components/inputs/badge-dropdown-list.hbs | 36 +++++++++ .../components/inputs/badge-dropdown-list.ts | 13 ++++ web/app/components/inputs/product-select.hbs | 55 +++++--------- web/app/components/inputs/product-select.ts | 3 + .../inputs/select-dropdown-list.hbs | 19 +++++ .../components/inputs/select-dropdown-list.ts | 13 ++++ web/app/components/new/doc-form.hbs | 74 +++++-------------- web/app/components/new/doc-form.ts | 19 ++--- web/app/components/x/dropdown-list/index.hbs | 16 ++++ .../x/dropdown-list/toggle-select.hbs | 17 +++++ web/app/styles/app.scss | 3 + .../components/x/dropdown/toggle-select.scss | 16 ++++ web/app/styles/hds-overrides.scss | 4 + web/app/styles/typography.scss | 3 + web/app/types/hds-badge.d.ts | 5 ++ web/app/utils/get-product-id.ts | 8 +- 17 files changed, 197 insertions(+), 109 deletions(-) create mode 100644 web/app/components/inputs/badge-dropdown-list.hbs create mode 100644 web/app/components/inputs/badge-dropdown-list.ts create mode 100644 web/app/components/inputs/select-dropdown-list.hbs create mode 100644 web/app/components/inputs/select-dropdown-list.ts create mode 100644 web/app/components/x/dropdown-list/toggle-select.hbs create mode 100644 web/app/styles/components/x/dropdown/toggle-select.scss create mode 100644 web/app/styles/typography.scss create mode 100644 web/app/types/hds-badge.d.ts diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index d131d5de4..c5053135e 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -135,11 +135,11 @@ >Product/Area {{#if this.isDraft}}
    -
    {{else}} diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs new file mode 100644 index 000000000..26b6087b3 --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -0,0 +1,36 @@ + + <:anchor as |dd|> +
    + {{#if @isSaving}} +
    + +
    + {{/if}} + + + + +
    + + <:item as |dd|> + + + + +
    diff --git a/web/app/components/inputs/badge-dropdown-list.ts b/web/app/components/inputs/badge-dropdown-list.ts new file mode 100644 index 000000000..12f1bfded --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.ts @@ -0,0 +1,13 @@ +import Component from "@glimmer/component"; + +interface InputsBadgeDropdownListComponentSignature { + Args: { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: () => void; + }; +} + +export default class InputsBadgeDropdownListComponent extends Component {} diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 418879eb1..17ab92ea9 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,42 +1,25 @@ {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} - - <:anchor as |dd|> -
    - {{#if @isSaving}} -
    - -
    - {{/if}} - - - - -
    - - <:item as |dd|> - - - - -
    + {{#if @formatIsBadge}} + + {{else}} + + {{/if}} {{else if this.fetchProducts.isRunning}} {{else}} diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 428f4a83b..1631c9ea4 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -4,11 +4,14 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; +import { BadgeSize } from "hermes/types/hds-badge"; interface InputsProductSelectSignatureSignature { Args: { selected?: any; onChange: (value: any) => void; + badgeSize?: BadgeSize; + formatIsBadge?: boolean; }; } diff --git a/web/app/components/inputs/select-dropdown-list.hbs b/web/app/components/inputs/select-dropdown-list.hbs new file mode 100644 index 000000000..5f91e4fb8 --- /dev/null +++ b/web/app/components/inputs/select-dropdown-list.hbs @@ -0,0 +1,19 @@ + + <:anchor as |dd|> + + + <:item as |dd|> + + + + + diff --git a/web/app/components/inputs/select-dropdown-list.ts b/web/app/components/inputs/select-dropdown-list.ts new file mode 100644 index 000000000..e346c7c64 --- /dev/null +++ b/web/app/components/inputs/select-dropdown-list.ts @@ -0,0 +1,13 @@ +import Component from "@glimmer/component"; + +interface InputsSelectDropdownListComponentSignature { + Args: { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: () => void; + }; +} + +export default class InputsSelectDropdownListComponent extends Component {} diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index f5c56bed4..b35b6e6a7 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -20,11 +20,8 @@

    Create your {{@docType}}

    -

    Complete the following metadata to create your - {{@docType}} - and begin editing your draft.

    -
    +
    Title - Your title should succinctly outline the idea you're - proposing.
    @@ -61,52 +56,17 @@
    - - Product/Area - Specify the full name of the product or area this - {{@docType}} - belongs to. - {{#if @productAbbrevMappings}} - - - {{#each-in @productAbbrevMappings as |name|}} - - {{/each-in}} - - {{/if}} - - - - Product/Area abbreviation - {{#if this.formErrors.productAbbreviation}} - - {{this.formErrors.productAbbreviation}} - - {{/if}} - Product/Area abbreviation is automatically populated on - selecting the "Product/Area" option. - - +
    + + Product/Area   + + + +
    {{! Note: We are still refining the subscribe/follow feature set. As part of that effort we will be looking into how the concept @@ -163,7 +123,7 @@ /> - Add contributors + Contributors {{#if this.formErrors.contributors}} @@ -171,10 +131,10 @@ {{/if}} - If you're collaborating with others on this - {{@docType}}, add them here. The document will automatically be - shared with the collaborators specified here. You can also add - contributors later. + Your + {{@docType}} + will be shared with the collaborators specified here. You can also + add contributors later.
    diff --git a/web/app/components/new/doc-form.ts b/web/app/components/new/doc-form.ts index c7cde3b66..be61dc4ab 100644 --- a/web/app/components/new/doc-form.ts +++ b/web/app/components/new/doc-form.ts @@ -48,7 +48,7 @@ export default class NewDocFormComponent extends Component 0; } - /** - * The product abbreviation for the selected product area. - */ - protected get productAbbreviation() { - return this.args.productAbbrevMappings.get(this.productArea); - } /** * Sets `formRequirementsMet` and conditionally validates the form. */ @@ -124,12 +118,6 @@ export default class NewDocFormComponent extends Component diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 36e92c207..4fe5fdbac 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -1,3 +1,5 @@ +@use "./typography"; + @use "components/action"; @use "components/toolbar"; @use "components/tooltip"; @@ -7,6 +9,7 @@ @use "components/x-hds-tab"; @use "components/x/dropdown/list"; @use "components/x/dropdown/list-item"; +@use "components/x/dropdown/toggle-select"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss new file mode 100644 index 000000000..30eedb7ef --- /dev/null +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -0,0 +1,16 @@ +.x-dropdown-list-toggle-select { + @apply min-w-[200px] text-left; + + &.hds-button--size-medium { + @apply pl-2.5 pr-1.5; + } + + &.hds-button--color-secondary:not(:hover) { + @apply bg-color-surface-primary; + + .flight-icon { + @apply text-color-foreground-faint; + } + + } +} diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index b64d0ff37..e4489262c 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -34,6 +34,10 @@ .hds-badge-dropdown { @apply pr-6; + .hds-badge__text { + @apply min-w-[120px]; + } + + .dropdown-caret { @apply absolute right-1.5 top-1/2 -translate-y-1/2; } diff --git a/web/app/styles/typography.scss b/web/app/styles/typography.scss new file mode 100644 index 000000000..fa7284505 --- /dev/null +++ b/web/app/styles/typography.scss @@ -0,0 +1,3 @@ +.hermes-form-label { + @apply text-body-200 font-semibold flex text-color-foreground-strong; +} diff --git a/web/app/types/hds-badge.d.ts b/web/app/types/hds-badge.d.ts new file mode 100644 index 000000000..9c4ccab12 --- /dev/null +++ b/web/app/types/hds-badge.d.ts @@ -0,0 +1,5 @@ +export enum BadgeSize { + Small = "small", + Medium = "medium", + Large = "large", +} diff --git a/web/app/utils/get-product-id.ts b/web/app/utils/get-product-id.ts index 748cfc5dd..d3b421713 100644 --- a/web/app/utils/get-product-id.ts +++ b/web/app/utils/get-product-id.ts @@ -1,5 +1,11 @@ -export default function getProductId(productName: string): string | null { +export default function getProductId( + productName: string | null +): string | null { + if (!productName) { + return null; + } let product = productName.toLowerCase(); + switch (product) { case "boundary": case "consul": From 81c54b3fadddda52e6ee475b3c4576a14d769910 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 11:42:03 -0400 Subject: [PATCH 058/128] Improvements around productAbbreviations --- web/app/components/inputs/product-select.hbs | 4 +++ web/app/components/inputs/product-select.ts | 10 +++++-- .../inputs/select-dropdown-list.hbs | 1 + web/app/components/new/doc-form.hbs | 11 ++++---- web/app/components/new/doc-form.ts | 14 +++++++--- web/app/components/x/dropdown-list/index.ts | 8 ++++-- web/app/routes/authenticated/new/doc.js | 27 ------------------- .../styles/components/x/dropdown/list.scss | 2 +- web/app/styles/hds-overrides.scss | 4 --- web/app/templates/authenticated/new/doc.hbs | 5 +--- 10 files changed, 38 insertions(+), 48 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 17ab92ea9..d902af722 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -7,6 +7,8 @@ @listIsOrdered={{true}} @onItemClick={{this.onChange}} @selected={{@selected}} + @placement={{@placement}} + @isSaving={{@isSaving}} class="w-80" ...attributes /> @@ -16,6 +18,8 @@ @listIsOrdered={{true}} @onItemClick={{this.onChange}} @selected={{@selected}} + @placement={{@placement}} + @isSaving={{@isSaving}} class="w-80" ...attributes /> diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 1631c9ea4..56f09f543 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -1,3 +1,4 @@ +import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { inject as service } from "@ember/service"; import Component from "@glimmer/component"; @@ -9,7 +10,7 @@ import { BadgeSize } from "hermes/types/hds-badge"; interface InputsProductSelectSignatureSignature { Args: { selected?: any; - onChange: (value: any) => void; + onChange: (value: string, abbreviation: string) => void; badgeSize?: BadgeSize; formatIsBadge?: boolean; }; @@ -31,7 +32,12 @@ export default class InputsProductSelectSignature extends Component { diff --git a/web/app/components/inputs/select-dropdown-list.hbs b/web/app/components/inputs/select-dropdown-list.hbs index 5f91e4fb8..3e86c628a 100644 --- a/web/app/components/inputs/select-dropdown-list.hbs +++ b/web/app/components/inputs/select-dropdown-list.hbs @@ -3,6 +3,7 @@ @listIsOrdered={{@listIsOrdered}} @onItemClick={{@onItemClick}} @selected={{@selected}} + @placement={{@placement}} ...attributes > <:anchor as |dd|> diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index b35b6e6a7..64c40c702 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -32,6 +32,9 @@ as |F| > Title + + A succinct outline of the idea youʼre proposing. +
    @@ -62,9 +65,9 @@
    @@ -131,13 +134,11 @@ {{/if}} - Your - {{@docType}} - will be shared with the collaborators specified here. You can also - add contributors later. + People to share your doc with. You can also add contributors later.
    +
    diff --git a/web/app/components/new/doc-form.ts b/web/app/components/new/doc-form.ts index be61dc4ab..c1c077c03 100644 --- a/web/app/components/new/doc-form.ts +++ b/web/app/components/new/doc-form.ts @@ -1,5 +1,5 @@ import Component from "@glimmer/component"; -import { task, timeout } from "ember-concurrency"; +import { restartableTask, task, timeout } from "ember-concurrency"; import { inject as service } from "@ember/service"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; @@ -34,7 +34,6 @@ const AWAIT_DOC_CREATED_MODAL_DELAY = Ember.testing ? 0 : 1500; interface NewDocFormComponentSignature { Args: { - productAbbrevMappings: Map; docType: string; }; } @@ -49,6 +48,7 @@ export default class NewDocFormComponent extends Component { + this.input.focus(); + }); } /** diff --git a/web/app/routes/authenticated/new/doc.js b/web/app/routes/authenticated/new/doc.js index ad2204138..cb8b2d694 100644 --- a/web/app/routes/authenticated/new/doc.js +++ b/web/app/routes/authenticated/new/doc.js @@ -33,33 +33,6 @@ export default class AuthenticatedNewDocRoute extends Route { return RSVP.hash({ docType: params?.docType, - productAbbrevMappings: this.getProductAbbrevMappings(), }); } - - async getProductAbbrevMappings() { - const products = await this.fetchSvc - .fetch("/api/v1/products") - .then((resp) => resp.json()) - .catch((err) => { - console.log(`Error requesting products: ${err}`); - }); - - // Sort product names alphabetically - const sortedProducts = Object.keys(products) - .sort() - .reduce((accum, key) => { - accum[key] = products[key]; - return accum; - }, {}); - - // Convert to map of product or area name - // and abbreviation to make look ups easier - const productAbbrevMappings = new Map(); - Object.keys(sortedProducts).forEach((key) => { - productAbbrevMappings.set(key, products[key].abbreviation); - }); - - return productAbbrevMappings; - } } diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index c5762d751..02b8b6d79 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -15,7 +15,7 @@ } .x-dropdown-list-scroll-container { - @apply overflow-auto w-full relative; + @apply overflow-auto w-full relative rounded-b-md; } .x-dropdown-list-empty-state { diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index e4489262c..b64d0ff37 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -34,10 +34,6 @@ .hds-badge-dropdown { @apply pr-6; - .hds-badge__text { - @apply min-w-[120px]; - } - + .dropdown-caret { @apply absolute right-1.5 top-1/2 -translate-y-1/2; } diff --git a/web/app/templates/authenticated/new/doc.hbs b/web/app/templates/authenticated/new/doc.hbs index ed4ba6333..1a49077a6 100644 --- a/web/app/templates/authenticated/new/doc.hbs +++ b/web/app/templates/authenticated/new/doc.hbs @@ -1,6 +1,3 @@ {{page-title (concat "Create Your " @model.docType)}} - + From 7987cd6cce74da7d31f40b6055987fd9f7f4bf1b Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 14:16:30 -0400 Subject: [PATCH 059/128] Update checkable-item-test.ts --- .../components/x/dropdown-list/checkable-item-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/integration/components/x/dropdown-list/checkable-item-test.ts b/web/tests/integration/components/x/dropdown-list/checkable-item-test.ts index a8401eb49..854482cf9 100644 --- a/web/tests/integration/components/x/dropdown-list/checkable-item-test.ts +++ b/web/tests/integration/components/x/dropdown-list/checkable-item-test.ts @@ -9,7 +9,7 @@ const COUNT_SELECTOR = "[data-test-x-dropdown-list-checkable-item-count]"; module("Integration | Component | x/dropdown-list", function (hooks) { setupRenderingTest(hooks); - test("a filter input is shown for long lists", async function (assert) { + test("it renders as expected", async function (assert) { this.set("selected", false); this.set("count", null); From d2747984846adc57f3f67a24851e2dda19c13470 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 14:27:26 -0400 Subject: [PATCH 060/128] Fix scrolling issue --- web/app/components/x/dropdown-list/index.ts | 4 ++-- .../integration/components/x/dropdown-list/index-test.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index fd689edb3..2aea65f9d 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -91,8 +91,8 @@ export default class XDropdownListComponent extends Component< this._input = e; // Wait until the content is positioned before focusing the input. - next(() => { - this.input.focus(); + schedule("afterRender", () => { + this.input.focus({ preventScroll: true }); }); } 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 0c08266d1..4066d06ee 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -9,6 +9,7 @@ import { triggerEvent, triggerKeyEvent, waitFor, + waitUntil, } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import htmlElement from "hermes/utils/html-element"; @@ -71,8 +72,12 @@ module("Integration | Component | x/dropdown-list", function (hooks) { .dom("[data-test-x-dropdown-list-input]") .doesNotExist("The input is not shown"); + await click("[data-test-toggle]"); + this.set("items", LONG_ITEM_LIST); + await click("[data-test-toggle]"); + assert .dom("[data-test-x-dropdown-list-input]") .exists("The input is shown"); From 47f550a51368b1e05e17f736a0a340ea5b5ab7ae Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 14:34:11 -0400 Subject: [PATCH 061/128] Update index.ts --- web/app/components/x/dropdown-list/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 2aea65f9d..9952ad0ce 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -90,10 +90,12 @@ export default class XDropdownListComponent extends Component< @action registerAndFocusInput(e: HTMLInputElement) { this._input = e; - // Wait until the content is positioned before focusing the input. - schedule("afterRender", () => { - this.input.focus({ preventScroll: true }); - }); + /** + * The dropdown is initially positioned in the top of page-body, + * so we specify that the focus event should not scroll to it. + * Instead, we use FloatingUI to place the input in view. + */ + this.input.focus({ preventScroll: true }); } /** From 79bae0e8c0b4aa6dbf9681db78ce117235087e62 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 21:07:52 -0400 Subject: [PATCH 062/128] Stop propigation on the DropdownList --- web/app/components/x/dropdown-list/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 9952ad0ce..d210ea7b1 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -160,6 +160,9 @@ export default class XDropdownListComponent extends Component< event.preventDefault(); showContent(); + // Prevent the event from bubbling to the contentBody's keydown listener. + event.stopPropagation(); + // Wait for menuItemIDs to be set by `didInsertContent`. schedule("afterRender", () => { switch (event.key) { From 71a815d6f8bd86579c06efc5f6454b098d0b5f14 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 21:19:59 -0400 Subject: [PATCH 063/128] Add product abbreviation to dropdown --- web/app/components/inputs/product-select.hbs | 11 ++++++++++- web/app/components/inputs/select-dropdown-list.hbs | 7 +------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index d902af722..0fa5df3d8 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -22,7 +22,16 @@ @isSaving={{@isSaving}} class="w-80" ...attributes - /> + > + <:item as |dd|> + + {{dd.value}} + + {{dd.attrs.abbreviation}} + + + + {{/if}} {{else if this.fetchProducts.isRunning}} diff --git a/web/app/components/inputs/select-dropdown-list.hbs b/web/app/components/inputs/select-dropdown-list.hbs index 3e86c628a..95416ccef 100644 --- a/web/app/components/inputs/select-dropdown-list.hbs +++ b/web/app/components/inputs/select-dropdown-list.hbs @@ -10,11 +10,6 @@ <:item as |dd|> - - - + {{yield dd to="item"}} From 78be77ee4384c591643a7059ca215ba25f1f6abc Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 25 Apr 2023 21:45:23 -0400 Subject: [PATCH 064/128] Tweaked ProductDropdown --- web/app/components/inputs/product-select.hbs | 7 ++-- .../inputs/select-dropdown-list.hbs | 2 +- .../x/dropdown-list/toggle-select.hbs | 36 ++++++++++--------- .../components/x/dropdown/list-item.scss | 4 +-- .../components/x/dropdown/toggle-select.scss | 6 ++-- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 0fa5df3d8..243421d25 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -25,9 +25,10 @@ > <:item as |dd|> - {{dd.value}} - - {{dd.attrs.abbreviation}} + + {{dd.value}} + + {{dd.attrs.abbreviation}} diff --git a/web/app/components/inputs/select-dropdown-list.hbs b/web/app/components/inputs/select-dropdown-list.hbs index 95416ccef..0f42ab622 100644 --- a/web/app/components/inputs/select-dropdown-list.hbs +++ b/web/app/components/inputs/select-dropdown-list.hbs @@ -7,7 +7,7 @@ ...attributes > <:anchor as |dd|> - + <:item as |dd|> {{yield dd to="item"}} diff --git a/web/app/components/x/dropdown-list/toggle-select.hbs b/web/app/components/x/dropdown-list/toggle-select.hbs index 8783fbdcd..1b7e1ea69 100644 --- a/web/app/components/x/dropdown-list/toggle-select.hbs +++ b/web/app/components/x/dropdown-list/toggle-select.hbs @@ -1,17 +1,19 @@ - +
    + + +
    diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index 764b8664a..f76264230 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -14,10 +14,10 @@ } .flight-icon { - @apply text-color-foreground-action shrink-0; + @apply shrink-0; &.check { - @apply mr-2.5; + @apply mr-2.5 text-color-foreground-action; } &.sort-icon { diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss index 30eedb7ef..5d2456045 100644 --- a/web/app/styles/components/x/dropdown/toggle-select.scss +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -4,13 +4,15 @@ &.hds-button--size-medium { @apply pl-2.5 pr-1.5; } + .hds-button__icon + .hds-button__text { + @apply ml-2.5; + } &.hds-button--color-secondary:not(:hover) { @apply bg-color-surface-primary; - .flight-icon { + .caret { @apply text-color-foreground-faint; } - } } From fdddbf1d12fcc03b50014acb4c4d7c76b1ead447 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 26 Apr 2023 11:01:40 -0400 Subject: [PATCH 065/128] Improve ProductSelect dropdown --- web/app/components/inputs/product-select.hbs | 35 ++++++++++++++++--- web/app/components/inputs/product-select.ts | 4 +++ .../inputs/select-dropdown-list.hbs | 4 ++- .../components/x/dropdown/toggle-select.scss | 4 +++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index 243421d25..d75a08875 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -13,7 +13,7 @@ ...attributes /> {{else}} - + <:anchor as |dd|> + + + {{or @selected "--"}} + {{#if this.selectedProductAbbreviation}} + + {{this.selectedProductAbbreviation}} + + {{/if}} + + + <:item as |dd|> - + - {{dd.value}} - + {{dd.value}} + {{dd.attrs.abbreviation}} + {{#if dd.selected}} + + {{/if}} - + {{/if}} {{else if this.fetchProducts.isRunning}} diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 56f09f543..379df0d32 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -30,6 +30,10 @@ export default class InputsProductSelectSignature extends Component <:anchor as |dd|> - + + {{yield to="anchor"}} + <:item as |dd|> {{yield dd to="item"}} diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss index 5d2456045..7e22a6866 100644 --- a/web/app/styles/components/x/dropdown/toggle-select.scss +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -1,6 +1,10 @@ .x-dropdown-list-toggle-select { @apply min-w-[200px] text-left; + &.hds-button { + @apply justify-start; + } + &.hds-button--size-medium { @apply pl-2.5 pr-1.5; } From 925f089a91268fcdffd9b78816af295622766a89 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 26 Apr 2023 11:30:32 -0400 Subject: [PATCH 066/128] Replace ToggleSelect --- web/app/components/inputs/product-select.hbs | 8 ++++---- .../inputs/select-dropdown-list.hbs | 17 ----------------- .../components/inputs/select-dropdown-list.ts | 13 ------------- web/app/components/new/doc-form.hbs | 19 ++++++++++++------- web/app/components/x/dropdown-list/index.hbs | 16 ---------------- .../components/x/dropdown/toggle-select.scss | 2 +- 6 files changed, 17 insertions(+), 58 deletions(-) delete mode 100644 web/app/components/inputs/select-dropdown-list.hbs delete mode 100644 web/app/components/inputs/select-dropdown-list.ts diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index d75a08875..b5db0b59d 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -20,12 +20,12 @@ @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} - class="w-80" + class="w-[300px] max-h-[305px]" ...attributes > <:anchor as |dd|> {{or @selected "--"}} @@ -38,7 +38,7 @@ {{/if}} @@ -52,7 +52,7 @@ {{#if dd.selected}} {{/if}} diff --git a/web/app/components/inputs/select-dropdown-list.hbs b/web/app/components/inputs/select-dropdown-list.hbs deleted file mode 100644 index 4d18dab02..000000000 --- a/web/app/components/inputs/select-dropdown-list.hbs +++ /dev/null @@ -1,17 +0,0 @@ - - <:anchor as |dd|> - - {{yield to="anchor"}} - - - <:item as |dd|> - {{yield dd to="item"}} - - diff --git a/web/app/components/inputs/select-dropdown-list.ts b/web/app/components/inputs/select-dropdown-list.ts deleted file mode 100644 index e346c7c64..000000000 --- a/web/app/components/inputs/select-dropdown-list.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Component from "@glimmer/component"; - -interface InputsSelectDropdownListComponentSignature { - Args: { - items: any; - selected?: any; - listIsOrdered?: boolean; - isSaving?: boolean; - onItemClick: () => void; - }; -} - -export default class InputsSelectDropdownListComponent extends Component {} diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index 64c40c702..86d4b64fe 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -60,14 +60,19 @@
    - - Product/Area   - - +
    + + Product/Area   + + + + Where your doc should be categorized. + +
    @@ -134,11 +139,11 @@ {{/if}} - People to share your doc with. You can also add contributors later. + People to share your doc with. You can always add more later.
    -
    +
    diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index 5dd40c59c..cb6b21085 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -50,22 +50,6 @@ 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 - ) - ) contentIsShown=f.contentIsShown ) to="anchor" diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss index 7e22a6866..91b1aea1d 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 min-w-[200px] text-left; + @apply text-left; &.hds-button { @apply justify-start; From 3e38f3687553c0f858975f7a3325185d9cbfef97 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 1 May 2023 20:24:50 -0400 Subject: [PATCH 067/128] Delete toggle-select.hbs --- .../x/dropdown-list/toggle-select.hbs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 web/app/components/x/dropdown-list/toggle-select.hbs diff --git a/web/app/components/x/dropdown-list/toggle-select.hbs b/web/app/components/x/dropdown-list/toggle-select.hbs deleted file mode 100644 index 1b7e1ea69..000000000 --- a/web/app/components/x/dropdown-list/toggle-select.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
    - - -
    From c574acb9cef22d648b0ae7e1b4b1ebecb6ea9de6 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 2 May 2023 14:16:00 -0400 Subject: [PATCH 068/128] Clean up FloatDown API --- web/app/components/floating-u-i/content.hbs | 1 + web/app/components/floating-u-i/content.ts | 1 + web/app/components/floating-u-i/index.ts | 3 +++ web/app/components/x/dropdown-list/index.ts | 4 ++++ web/app/styles/components/x/dropdown/list.scss | 2 +- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/web/app/components/floating-u-i/content.hbs b/web/app/components/floating-u-i/content.hbs index 95d4d1006..5a3efd41d 100644 --- a/web/app/components/floating-u-i/content.hbs +++ b/web/app/components/floating-u-i/content.hbs @@ -8,6 +8,7 @@ {{will-destroy this.cleanup}} {{did-insert this.didInsert}} data-test-floating-ui-placement={{@placement}} + data-anchored-to={{@id}} id="floating-ui-content-{{@id}}" class="hermes-floating-ui-content" ...attributes diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 62c6d0ee1..cb79314e0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -15,6 +15,7 @@ import { tracked } from "@glimmer/tracking"; interface FloatingUIContentSignature { Args: { anchor: HTMLElement; + id: string; placement?: Placement; renderOut?: boolean; }; diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index aeea89c09..3c70616f1 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -26,6 +26,9 @@ export default class FloatingUIComponent extends Component { switch (event.key) { diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index c5762d751..02b8b6d79 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -15,7 +15,7 @@ } .x-dropdown-list-scroll-container { - @apply overflow-auto w-full relative; + @apply overflow-auto w-full relative rounded-b-md; } .x-dropdown-list-empty-state { From e6f9a45ea0e716a024883e82459e313da1966d7f Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 2 May 2023 14:28:01 -0400 Subject: [PATCH 069/128] Replace FacetDropdowns --- .../header/facet-dropdown-list-item.hbs | 46 --- .../header/facet-dropdown-list-item.ts | 187 ----------- .../components/header/facet-dropdown-list.hbs | 70 ---- .../components/header/facet-dropdown-list.ts | 167 ---------- web/app/components/header/facet-dropdown.hbs | 69 +--- web/app/components/header/facet-dropdown.ts | 311 +----------------- web/app/components/header/sort-dropdown.hbs | 30 ++ web/app/components/header/sort-dropdown.ts | 29 ++ web/app/components/header/toolbar.hbs | 7 +- web/app/components/header/toolbar.ts | 9 +- web/app/styles/app.scss | 1 - .../components/header/facet-dropdown.scss | 47 --- web/app/styles/hds-overrides.scss | 2 +- .../header/facet-dropdown-list-item-test.ts | 172 ---------- .../header/facet-dropdown-list-test.ts | 271 --------------- .../components/header/facet-dropdown-test.ts | 149 --------- .../components/header/toolbar-test.ts | 30 +- 17 files changed, 108 insertions(+), 1489 deletions(-) delete mode 100644 web/app/components/header/facet-dropdown-list-item.hbs delete mode 100644 web/app/components/header/facet-dropdown-list-item.ts delete mode 100644 web/app/components/header/facet-dropdown-list.hbs delete mode 100644 web/app/components/header/facet-dropdown-list.ts create mode 100644 web/app/components/header/sort-dropdown.hbs create mode 100644 web/app/components/header/sort-dropdown.ts delete mode 100644 web/app/styles/components/header/facet-dropdown.scss delete mode 100644 web/tests/integration/components/header/facet-dropdown-list-item-test.ts delete mode 100644 web/tests/integration/components/header/facet-dropdown-list-test.ts delete mode 100644 web/tests/integration/components/header/facet-dropdown-test.ts diff --git a/web/app/components/header/facet-dropdown-list-item.hbs b/web/app/components/header/facet-dropdown-list-item.hbs deleted file mode 100644 index 5beeb545b..000000000 --- a/web/app/components/header/facet-dropdown-list-item.hbs +++ /dev/null @@ -1,46 +0,0 @@ -{{! @glint-nocheck: not typesafe yet }} -
  • - - {{#if this.sortByQueryParams}} - - {{else}} - - {{/if}} -
    - - {{@value}} - - {{#unless this.sortByQueryParams}} - - {{/unless}} -
    -
    -
  • diff --git a/web/app/components/header/facet-dropdown-list-item.ts b/web/app/components/header/facet-dropdown-list-item.ts deleted file mode 100644 index 650a339bb..000000000 --- a/web/app/components/header/facet-dropdown-list-item.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { action } from "@ember/object"; -import RouterService from "@ember/routing/router-service"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; -import { FocusDirection } from "./facet-dropdown"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { next } from "@ember/runloop"; -import { SortByLabel, SortByValue } from "./toolbar"; - -enum FacetDropdownAriaRole { - Option = "option", - Menuitem = "menuitem", -} - -interface HeaderFacetDropdownListItemComponentSignature { - Element: HTMLAnchorElement; - Args: { - /** - * The label of the facet, e.g., "Type" or "Owner." - * Used to construct filter query params. - */ - label: string; - /** - * The index of the currently focused item. - * Used to determine aria-focus. - */ - focusedItemIndex: number; - /** - * The name of the facet, e.g., "Approved," "In-Review." - * Used primarily to construct the query params. - */ - value: string; - /** - * The role of the list item, e.g., "option" or "menuitem". - * The if the facetDropdown has a filter input, the role is "option". - * Otherwise, it's "menuitem". - */ - role: FacetDropdownAriaRole; - /** - * The number of matches associated with the filter. - * Used to display the badge count. - */ - count: number; - /** - * Whether the item an actively applied filter. - * Used for checkmark-display logic, and as - * params for the `get-facet-query-hash` helper - */ - selected: boolean; - /** - * If the dropdown list is the sort control, the current sort value. - * Used to determine whether to use the `get-facet-query-hash` helper - * or this class's sortByQueryParams getter. - */ - currentSortByValue?: SortByValue; - /** - * The action called to hide the dropdown. - * Used to close the dropdown on the next run loop. - */ - hideDropdown: () => void; - /** - * The action called on mouseenter that sets the focused-item index value. - * Includes a `maybeScrollIntoView` argument that we use to disable - * mouse-activated scroll manipulation. - */ - setFocusedItemIndex: ( - focusDirection: FocusDirection | number, - maybeScrollIntoView?: boolean - ) => void; - }; -} - -export default class HeaderFacetDropdownListItemComponent extends Component { - @service declare router: RouterService; - - /** - * The element reference, set on insertion and updated on mouseenter. - * Used to compute the element's ID, which may change when the list is filtered. - */ - @tracked private _domElement: HTMLElement | null = null; - - /** - * An asserted-true reference to the element. - */ - protected get domElement() { - assert("element must exist", this._domElement); - return this._domElement; - } - - /** - * The element's domID, e.g., "facet-dropdown-list-item-0" - * Which is computed by the parent component on render and when - * the FacetList is filtered. Parsed by `this.id` to get the - * numeric identifier for the element. - */ - private get domElementID() { - return this.domElement.id; - } - - /** - * The current route name, used to set the LinkTo's @route - */ - protected get currentRouteName(): string { - return this.router.currentRouteName; - } - - /** - * A numeric identifier for the element based on its id, - * as computed by the parent component on render and when - * the FacetList is filtered. Strips everything but the trailing number. - * Used to apply classes and aria-selected, and to direct the parent component's - * focus action toward the correct element. - * Regex reference: - * \d = Any digit 0-9 - * + = One or more of the preceding token - * $ = End of input - */ - protected get itemIndexNumber(): number { - return parseInt(this.domElementID.match(/\d+$/)?.[0] || "0", 10); - } - - /** - * Whether the element is aria-selected. - * Used to determine whether to apply the "focused" class - * and to set the `aria-selected` attribute. - */ - protected get isAriaSelected(): boolean { - if (!this._domElement) { - // True when first computed, which happens - // before the element is inserted and registered. - return false; - } - if (this.args.focusedItemIndex === -1) { - return false; - } - return this.args.focusedItemIndex === this.itemIndexNumber; - } - - /** - * The query hash to use when the a sortBy filter is selected. - */ - protected get sortByQueryParams(): { sortBy: SortByValue } | void { - // The sortBy filter is the only facet that passes this argument. - if (!this.args.currentSortByValue) { - return; - } else { - switch (this.args.value) { - case SortByLabel.Newest: - return { sortBy: SortByValue.DateDesc }; - case SortByLabel.Oldest: - return { sortBy: SortByValue.DateAsc }; - } - } - } - - /** - * Sets our local `element` reference to mouse target, - * to capture its ID, which may change when the list is filtered. - * Then, calls the parent component's `setFocusedItemIndex` action, - * directing focus to the current element. - */ - @action protected focusMouseTarget(e: MouseEvent) { - let target = e.target; - assert("target must be an element", target instanceof HTMLElement); - this._domElement = target; - this.args.setFocusedItemIndex(this.itemIndexNumber, false); - } - - /** - * Closes the dropdown on the next run loop. - * Done so we don't interfere with Ember's handling. - */ - @action protected delayedCloseDropdown() { - next(() => { - this.args.hideDropdown(); - }); - } - - /** - * The action called on element insertion. Sets the local `element` - * reference to the domElement we know to be our target. - */ - @action protected registerElement(element: HTMLElement) { - this._domElement = element; - } -} diff --git a/web/app/components/header/facet-dropdown-list.hbs b/web/app/components/header/facet-dropdown-list.hbs deleted file mode 100644 index 2c97559b2..000000000 --- a/web/app/components/header/facet-dropdown-list.hbs +++ /dev/null @@ -1,70 +0,0 @@ -{{! @glint-nocheck: not typesafe yet }} -{{on-document "keydown" this.maybeKeyboardNavigate}} -
    - {{#if @inputIsShown}} -
    - -
    - {{/if}} -
    - {{#if this.noMatchesFound}} -
    - No matches -
    - {{else}} -
      - {{#each-in @shownFacets as |value attrs|}} - - {{/each-in}} -
    - {{/if}} -
    -
    diff --git a/web/app/components/header/facet-dropdown-list.ts b/web/app/components/header/facet-dropdown-list.ts deleted file mode 100644 index 0310fbbba..000000000 --- a/web/app/components/header/facet-dropdown-list.ts +++ /dev/null @@ -1,167 +0,0 @@ -import Component from "@glimmer/component"; -import { FacetDropdownObjects } from "hermes/types/facets"; -import { action } from "@ember/object"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { inject as service } from "@ember/service"; -import RouterService from "@ember/routing/router-service"; -import { FocusDirection } from "./facet-dropdown"; - -interface HeaderFacetDropdownListComponentSignature { - Args: { - /** - * The facet's label, e.g., "Type," "Status." - * Used to construct facet query hashes. - */ - label: string; - /** - * Whether the facet dropdown has a filter input. - * Used to set the correct aria role for the containers, lists, and list items, - * and to determine the correct className for the dropdown. - */ - inputIsShown: boolean; - /** - * The facets that should be shown in the dropdown. - * Looped through in the tamplate to render the list items. - * Used to determine whether a "No matches found" message should be shown. - */ - shownFacets: FacetDropdownObjects; - /** - * The popover element, registered when its element is inserted. - * Used to scope our querySelector calls. - */ - popoverElement: HTMLDivElement | null; - /** - * The role of the list items. - * Used in querySelector calls to specify the correct list items. - */ - listItemRole: "option" | "menuitem"; - /** - * An action called to reset the focusedItem index. - * Used on input focusin, which happens on dropdown reveal. - **/ - resetFocusedItemIndex: () => void; - /** - * The action run when the user types in the filter input. - * Used to filter the shownFacets. - */ - onInput: (event: InputEvent) => void; - /** - * The action run when the popover is inserted into the DOM. - * Used to register the popover element for use in querySelector calls. - */ - registerPopover: (element: HTMLDivElement) => void; - /** - * The action to move the focusedItemIndex within the dropdown. - * Used on ArrowUp/ArrowDown/Enter keydown events, - * and to pass to our child element for mouseenter events. - */ - setFocusedItemIndex: (direction: FocusDirection) => void; - /** - * The action to hide the dropdown. - * Called when the user presses the Enter key on a selection, - * and passed to our child element for click events. - */ - hideDropdown: () => void; - }; -} - -export enum FacetNames { - DocType = "docType", - Owners = "owners", - Status = "status", - Product = "product", -} - -export default class HeaderFacetDropdownListComponent extends Component { - @service declare router: RouterService; - - /** - * The input element, registered when its element is inserted - * and asserted to exist in the inputElement getter. - */ - @tracked private _inputElement: HTMLInputElement | null = null; - - /** - * The name of the current route. - * Used to determine the component's LinkTo route. - */ - protected get currentRouteName(): string { - return this.router.currentRouteName; - } - - /** - * The input element. - * Receives focus when the user presses the up arrow key - * while the first menu item is focused. - */ - private get inputElement(): HTMLInputElement { - assert("_inputElement must exist", this._inputElement); - return this._inputElement; - } - - /** - * Whether there are no matches found for the user's input. - * Used to show a "No matches found" message in the template. - */ - protected get noMatchesFound(): boolean { - if (!this.args.inputIsShown) { - return false; - } - return Object.entries(this.args.shownFacets).length === 0; - } - - /** - * The code-friendly name of the facet. - * Used to apply width styles to the dropdown. - */ - protected get facetName(): FacetNames | undefined { - switch (this.args.label) { - case "Type": - return FacetNames.DocType; - case "Status": - return FacetNames.Status; - case "Product/Area": - return FacetNames.Product; - case "Owner": - return FacetNames.Owners; - } - } - - /** - * Registers the input element. - * Used to assert that the element exists and can be focused. - */ - @action protected registerAndFocusInput(element: HTMLInputElement) { - this._inputElement = element; - this.inputElement.focus(); - } - - /** - * The action run when the user presses a key. - * Handles the arrow keys to navigate the dropdown or - * hits Enter to select the focused item. - */ - @action protected maybeKeyboardNavigate(event: KeyboardEvent) { - if (event.key === "ArrowDown") { - event.preventDefault(); - this.args.setFocusedItemIndex(FocusDirection.Next); - } - - if (event.key === "ArrowUp") { - event.preventDefault(); - this.args.setFocusedItemIndex(FocusDirection.Previous); - } - - if (event.key === "Enter") { - event.preventDefault(); - assert("popoverElement must exist", this.args.popoverElement); - const target = this.args.popoverElement.querySelector("[aria-selected]"); - - if (target instanceof HTMLAnchorElement) { - target.click(); - this.args.hideDropdown(); - } - } - } -} diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 8799b41e3..5fe7ccec3 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -1,54 +1,19 @@ {{! @glint-nocheck: not typesafe yet }} -{{! - Marked up with guidance from: - https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ - https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/ - https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/ - https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions-active-descendant/ -}} -
    - - {{#if this.dropdownIsShown}} - - {{/if}} -
    + + <:anchor as |dd|> + + + <:item as |dd|> + + + + + diff --git a/web/app/components/header/facet-dropdown.ts b/web/app/components/header/facet-dropdown.ts index fc41a767b..6cc61043f 100644 --- a/web/app/components/header/facet-dropdown.ts +++ b/web/app/components/header/facet-dropdown.ts @@ -1,10 +1,7 @@ import Component from "@glimmer/component"; -import { action } from "@ember/object"; import { FacetDropdownObjects } from "hermes/types/facets"; -import { tracked } from "@glimmer/tracking"; -import { assert } from "@ember/debug"; -import { restartableTask } from "ember-concurrency"; -import { schedule } from "@ember/runloop"; +import { inject as service } from "@ember/service"; +import RouterService from "@ember/routing/router-service"; interface FacetDropdownComponentSignature { Args: { @@ -14,308 +11,10 @@ interface FacetDropdownComponentSignature { }; } -export enum FocusDirection { - Previous = "previous", - Next = "next", - First = "first", - Last = "last", -} - export default class FacetDropdownComponent extends Component { - @tracked private _triggerElement: HTMLButtonElement | null = null; - @tracked private _scrollContainer: HTMLElement | null = null; - @tracked private _popoverElement: HTMLDivElement | null = null; - - @tracked protected query: string = ""; - @tracked protected listItemRole = this.inputIsShown ? "option" : "menuitem"; - @tracked protected dropdownIsShown = false; - @tracked protected focusedItemIndex = -1; - @tracked protected _shownFacets: FacetDropdownObjects | null = null; - - /** - * The dropdown menu items. Registered on insert and - * updated with on keydown and filterInput events. - * Used to determine the list length, and to find the focused - * element by index. - */ - @tracked protected menuItems: NodeListOf | null = null; - - /** - * An asserted-true reference to the scroll container. - * Used in the `maybeScrollIntoView` calculations. - */ - private get scrollContainer(): HTMLElement { - assert("_scrollContainer must exist", this._scrollContainer); - return this._scrollContainer; - } - - /** - * An asserted-true reference to the popover div. - * Used to scope querySelectorAll calls. - */ - private get popoverElement(): HTMLDivElement { - assert("_popoverElement must exist", this._popoverElement); - return this._popoverElement; - } - - /** - * The dropdown trigger. - * Passed to the dismissible modifier as a dropdown relative. - */ - protected get triggerElement(): HTMLButtonElement { - assert("_triggerElement must exist", this._triggerElement); - return this._triggerElement; - } - - /** - * The facets that should be shown in the dropdown. - * Initially the same as the facets passed in and - * updated when the user types in the filter input. - */ - protected get shownFacets(): FacetDropdownObjects { - if (this._shownFacets) { - return this._shownFacets; - } else { - return this.args.facets; - } - } - - /** - * Whether the filter input should be shown. - * True when the input has more facets than - * can be shown in the dropdown (12). - */ - protected get inputIsShown() { - return Object.entries(this.args.facets).length > 12; - } + @service declare router: RouterService; - @action protected registerTrigger(element: HTMLButtonElement) { - this._triggerElement = element; + protected get currentRouteName() { + return this.router.currentRouteName; } - - @action protected registerPopover(element: HTMLDivElement) { - this._popoverElement = element; - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - } - - @action protected registerScrollContainer(element: HTMLDivElement) { - this._scrollContainer = element; - } - - /** - * The action run when the popover is inserted, and when - * the user filters or navigates the dropdown. - * Loops through the menu items and assigns an id that - * matches the index of the item in the list. - */ - @action assignMenuItemIDs(items: NodeListOf): void { - this.menuItems = items; - for (let i = 0; i < items.length; i++) { - let item = items[i]; - assert("item must exist", item instanceof HTMLElement); - item.id = `facet-dropdown-menu-item-${i}`; - } - } - - /** - * The action run when the user presses a key. - * Handles the arrow keys to navigate the dropdown. - */ - @action protected onKeydown(event: KeyboardEvent) { - if (event.key === "ArrowDown") { - event.preventDefault(); - this.setFocusedItemIndex(FocusDirection.Next); - } - if (event.key === "ArrowUp") { - event.preventDefault(); - this.setFocusedItemIndex(FocusDirection.Previous); - } - } - - /** - * Toggles the dropdown visibility. - * Called when the user clicks on the dropdown trigger. - */ - @action protected toggleDropdown(): void { - if (this.dropdownIsShown) { - this.hideDropdown(); - } else { - this.showDropdown(); - } - } - - @action protected showDropdown(): void { - this.dropdownIsShown = true; - schedule("afterRender", () => { - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); - } - - /** - * The action run when the user clicks outside the dropdown. - * Hides the dropdown and resets the various tracked states. - */ - @action protected hideDropdown(): void { - this.query = ""; - this.dropdownIsShown = false; - this._shownFacets = null; - this.resetFocusedItemIndex(); - } - - /** - * The action run when the trigger is focused and the user - * presses the up or down arrow keys. Used to open and focus - * to the first or last item in the dropdown. - */ - @action protected onTriggerKeydown(event: KeyboardEvent) { - if (this.dropdownIsShown) { - return; - } - - if (event.key === "ArrowUp" || event.key === "ArrowDown") { - event.preventDefault(); - this.showDropdown(); - - // Stop the event from bubbling to the popover's keydown handler. - event.stopPropagation(); - - // Wait for the menuItems to be set by the showDropdown action. - schedule("afterRender", () => { - switch (event.key) { - case "ArrowDown": - this.setFocusedItemIndex(FocusDirection.First, false); - break; - case "ArrowUp": - this.setFocusedItemIndex(FocusDirection.Last); - break; - } - }); - } - } - - /** - * Sets the focus to the next or previous menu item. - * Used by the onKeydown action to navigate the dropdown, and - * by the FacetDropdownListItem component on mouseenter.s - */ - @action protected setFocusedItemIndex( - focusDirectionOrNumber: FocusDirection | number, - maybeScrollIntoView = true - ) { - let { menuItems, focusedItemIndex } = this; - - let setFirst = () => { - focusedItemIndex = 0; - }; - - let setLast = () => { - assert("menuItems must exist", menuItems); - focusedItemIndex = menuItems.length - 1; - }; - - if (!menuItems) { - return; - } - - if (menuItems.length === 0) { - return; - } - - switch (focusDirectionOrNumber) { - case FocusDirection.Previous: - if (focusedItemIndex === -1 || focusedItemIndex === 0) { - // When the first or no item is focused, "previous" focuses the last item. - setLast(); - } else { - focusedItemIndex--; - } - break; - case FocusDirection.Next: - if (focusedItemIndex === menuItems.length - 1) { - // When the last item is focused, "next" focuses the first item. - setFirst(); - } else { - focusedItemIndex++; - } - break; - case FocusDirection.First: - setFirst(); - break; - case FocusDirection.Last: - setLast(); - break; - default: - focusedItemIndex = focusDirectionOrNumber; - break; - } - - this.focusedItemIndex = focusedItemIndex; - - if (maybeScrollIntoView) { - this.maybeScrollIntoView(); - } - } - - /** - * Checks whether the focused item is completely visible, - * and, if necessary, scrolls the dropdown to make it visible. - * Used by the setFocusedItemIndex action on keydown. - */ - private maybeScrollIntoView() { - const focusedItem = this.menuItems?.item(this.focusedItemIndex); - assert("focusedItem must exist", focusedItem instanceof HTMLElement); - - const containerTopPadding = 12; - const containerHeight = this.scrollContainer.offsetHeight; - const itemHeight = focusedItem.offsetHeight; - const itemTop = focusedItem.offsetTop; - const itemBottom = focusedItem.offsetTop + itemHeight; - const scrollviewTop = this.scrollContainer.scrollTop - containerTopPadding; - const scrollviewBottom = scrollviewTop + containerHeight; - - if (itemBottom > scrollviewBottom) { - this.scrollContainer.scrollTop = itemTop + itemHeight - containerHeight; - } else if (itemTop < scrollviewTop) { - this.scrollContainer.scrollTop = itemTop; - } - } - - /** - * Resets the focus index to its initial value. - * Called when the dropdown is closed, and when the input is focused. - */ - @action protected resetFocusedItemIndex() { - this.focusedItemIndex = -1; - } - - /** - * The action run when the user types in the input. - * Filters the facets shown in the dropdown and schedules - * the menu items to be assigned their new IDs. - */ - protected onInput = restartableTask(async (inputEvent: InputEvent) => { - this.focusedItemIndex = -1; - - let shownFacets: FacetDropdownObjects = {}; - let facets = this.args.facets; - - this.query = (inputEvent.target as HTMLInputElement).value; - for (const [key, value] of Object.entries(facets)) { - if (key.toLowerCase().includes(this.query.toLowerCase())) { - shownFacets[key] = value; - } - } - - this._shownFacets = shownFacets; - - schedule("afterRender", () => { - this.assignMenuItemIDs( - this.popoverElement.querySelectorAll(`[role=${this.listItemRole}]`) - ); - }); - }); } diff --git a/web/app/components/header/sort-dropdown.hbs b/web/app/components/header/sort-dropdown.hbs new file mode 100644 index 000000000..959b9bbe6 --- /dev/null +++ b/web/app/components/header/sort-dropdown.hbs @@ -0,0 +1,30 @@ +{{! @glint-nocheck: not typesafe yet }} + + + <:anchor as |dd|> + + + <:item as |dd|> + + + {{dd.value}} + + + diff --git a/web/app/components/header/sort-dropdown.ts b/web/app/components/header/sort-dropdown.ts new file mode 100644 index 000000000..3b2142f21 --- /dev/null +++ b/web/app/components/header/sort-dropdown.ts @@ -0,0 +1,29 @@ +import Component from "@glimmer/component"; +import { SortByFacets, SortByValue } from "./toolbar"; +import { inject as service } from "@ember/service"; +import RouterService from "@ember/routing/router-service"; + +interface HeaderSortDropdownComponentSignature { + Args: { + label: string; + facets: SortByFacets; + disabled: boolean; + currentSortByValue: SortByValue; + }; +} + +export default class HeaderSortDropdownComponent extends Component { + @service declare router: RouterService; + + get currentRouteName() { + return this.router.currentRouteName; + } + + get dateDesc() { + return SortByValue.DateDesc; + } + + get dateAsc() { + return SortByValue.DateAsc; + } +} diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index b969e24de..bb1031cee 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -34,13 +34,12 @@
    {{#if (and @facets (not @sortControlIsHidden))}} - {{/if}}
    diff --git a/web/app/components/header/toolbar.ts b/web/app/components/header/toolbar.ts index bea03986c..16f29e379 100644 --- a/web/app/components/header/toolbar.ts +++ b/web/app/components/header/toolbar.ts @@ -27,6 +27,13 @@ export enum FacetName { Product = "product", } +export interface SortByFacets { + [name: string]: { + count: number; + selected: boolean; + }; +} + export type ActiveFilters = { [name in FacetName]: string[]; }; @@ -117,7 +124,7 @@ export default class ToolbarComponent extends Component {}); - }); - - test("the check mark is visible if the filter is selected", async function (assert) { - this.set("isSelected", false); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - assert - .dom("[data-test-facet-dropdown-list-item-check]") - .hasStyle({ visibility: "hidden" }, "check is initially hidden"); - - this.set("isSelected", true); - - assert - .dom("[data-test-facet-dropdown-list-item-check]") - .hasStyle( - { visibility: "visible" }, - 'check is visible when "selected" is true' - ); - }); - - test("filters display a badge count and sort controls show an icon", async function (assert) { - this.set("currentSortByValue", undefined); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - assert - .dom("[data-test-facet-dropdown-menu-item-count]") - .hasText("15", "badge count is displayed"); - assert - .dom("[data-test-facet-dropdown-list-item-sort-icon]") - .doesNotExist( - "sort icon isn't shown unless `currentSortByValue` is defined" - ); - - this.set("currentSortByValue", SortByValue.DateAsc); - - assert - .dom("[data-test-facet-dropdown-list-item-sort-icon]") - .exists("sort icon is shown"); - assert - .dom("[data-test-facet-dropdown-menu-item-count]") - .doesNotExist( - "badge count isn't shown when `currentSortByValue` is defined" - ); - }); - - test("it has the correct queryParams", async function (assert) { - this.set("currentSortByValue", null); - this.set("value", "Approved"); - - const activeFiltersService = this.owner.lookup( - "service:active-filters" - ) as ActiveFiltersService; - - activeFiltersService.index = { - docType: [], - owners: [], - product: [], - status: [], - }; - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - assert - .dom("[data-test-facet-dropdown-menu-item-link]") - .hasAttribute( - "href", - "/all?status=%5B%22Approved%22%5D", - "filter queryParams are correct" - ); - - this.set("currentSortByValue", SortByValue.DateAsc); - this.set("value", "Oldest"); - - assert - .dom("[data-test-facet-dropdown-menu-item-link]") - .hasAttribute( - "href", - `/all?sortBy=${SortByValue.DateAsc}`, - "sort queryParams are correct" - ); - }); - - test("it gets aria-focused on mouseenter", async function (assert) { - this.set("isSelected", false); - this.set("focusedItemIndex", -1); - this.set("setFocusedItemIndex", (focusDirection: number) => { - this.set("focusedItemIndex", focusDirection); - }); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - const listItemSelector = "[data-test-facet-dropdown-menu-item-link]"; - - assert.dom(listItemSelector).doesNotHaveClass("is-aria-selected"); - assert.dom(listItemSelector).doesNotHaveAttribute("aria-selected"); - - await triggerEvent(listItemSelector, "mouseenter"); - assert.dom(listItemSelector).hasClass("is-aria-selected"); - assert.dom(listItemSelector).hasAttribute("aria-selected"); - }); - } -); diff --git a/web/tests/integration/components/header/facet-dropdown-list-test.ts b/web/tests/integration/components/header/facet-dropdown-list-test.ts deleted file mode 100644 index f747b367d..000000000 --- a/web/tests/integration/components/header/facet-dropdown-list-test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "ember-qunit"; -import { find, findAll, render, triggerKeyEvent } from "@ember/test-helpers"; -import { assert as emberAssert } from "@ember/debug"; -import { hbs } from "ember-cli-htmlbars"; -import { LONG_FACET_LIST, SHORT_FACET_LIST } from "./facet-dropdown-test"; -import { FocusDirection } from "hermes/components/header/facet-dropdown"; - -module( - "Integration | Component | header/facet-dropdown-list", - function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("popoverElement", null); - this.set("registerPopover", (element: HTMLDivElement) => { - this.set("popoverElement", element); - }); - this.set("focusedItemIndex", -1); - this.set("scrollContainer", null); - this.set("registerScrollContainer", (element: HTMLDivElement) => { - this.set("scrollContainer", element); - }); - this.set("resetFocusedItemIndex", () => { - this.set("focusedItemIndex", -1); - }); - this.set("onInput", () => {}); - this.set("setFocusedItemIndex", (direction: FocusDirection) => { - const currentIndex = this.get("focusedItemIndex"); - emberAssert( - "currentIndex must be a number", - typeof currentIndex === "number" - ); - - const numberOfItems = findAll( - "[data-test-facet-dropdown-menu-item]" - ).length; - - if (direction === FocusDirection.Next) { - if (currentIndex === numberOfItems - 1) { - this.set("focusedItemIndex", 0); - return; - } else { - this.set("focusedItemIndex", currentIndex + 1); - return; - } - } - if (direction === FocusDirection.Previous) { - if (currentIndex === 0) { - this.set("focusedItemIndex", numberOfItems - 1); - return; - } else { - this.set("focusedItemIndex", currentIndex - 1); - return; - } - } - }); - }); - - test("keyboard navigation works as expected (long list)", async function (assert) { - this.set("shownFacets", LONG_FACET_LIST); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - let inputSelector = "[data-test-facet-dropdown-input]"; - let input = find(inputSelector); - - emberAssert("input must exist", input); - - assert.equal(document.activeElement, input, "The input is autofocused"); - assert.dom("[data-test-facet-dropdown-popover]").hasAttribute("role", "combobox"); - assert - .dom(inputSelector) - .doesNotHaveAttribute( - "aria-activedescendant", - "No items are aria-focused yet" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "When no items are aria-focused, ArrowDown moves aria-focus to the first item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "ArrowDown moves aria-focus to the next item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowUp"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "ArrowUp moves aria-focus to the previous item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowUp"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-12", - "Keying up on the first item aria-focuses the last item" - ); - - await triggerKeyEvent(input, "keydown", "ArrowDown"); - assert - .dom(inputSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "Keying down on the last item aria-focuses the first item" - ); - }); - - test("keyboard navigation works as expected (short list)", async function (assert) { - this.set("shownFacets", SHORT_FACET_LIST); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - let menuSelector = "[data-test-facet-dropdown-menu]"; - let menu = find(menuSelector); - - emberAssert("menu must exist", menu); - - assert - .dom(menuSelector) - .doesNotHaveAttribute( - "aria-activedescendant", - "No items are aria-focused yet" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowDown"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "When no items are aria-focused, ArrowDown moves aria-focus to the first item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowDown"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "ArrowDown moves aria-focus to the next item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowUp"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-0", - "ArrowUp moves aria-focus to the previous item" - ); - - await triggerKeyEvent(menu, "keydown", "ArrowUp"); - assert - .dom(menuSelector) - .hasAttribute( - "aria-activedescendant", - "facet-dropdown-menu-item-1", - "Keying up on the first item aria-focuses the last item" - ); - }); - - test("it applies the correct classNames to the popover", async function (assert) { - this.set("shownFacets", SHORT_FACET_LIST); - this.set("label", "Status"); - this.set("inputIsShown", false); - - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - let popoverSelector = "[data-test-facet-dropdown-popover]"; - let popover = find(popoverSelector); - - emberAssert("popover must exist", popover); - - assert.dom(popoverSelector).doesNotHaveClass("large"); - assert - .dom(popoverSelector) - .hasClass("medium", 'the status facet has a "medium" class'); - - this.set("label", "Type"); - - assert.dom(popoverSelector).doesNotHaveClass("large"); - assert - .dom(popoverSelector) - .doesNotHaveClass( - "medium", - 'only the status facet has a "medium" class' - ); - - this.set("inputIsShown", true); - - assert - .dom(popoverSelector) - .hasClass("large", 'facets with inputs have a "large" class'); - - this.set("label", "Status"); - - assert.dom(popoverSelector).hasClass("large"); - assert - .dom(popoverSelector) - .doesNotHaveClass( - "medium", - "because the status facet has an input, the medium class is not applied" - ); - }); - } -); diff --git a/web/tests/integration/components/header/facet-dropdown-test.ts b/web/tests/integration/components/header/facet-dropdown-test.ts deleted file mode 100644 index 7d2acd5e8..000000000 --- a/web/tests/integration/components/header/facet-dropdown-test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "ember-qunit"; -import { click, fillIn, render, triggerKeyEvent } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; - -export const SHORT_FACET_LIST = { - RFC: { - count: 10, - selected: false, - }, - PRD: { - count: 5, - selected: true, - }, -}; - -export const LONG_FACET_LIST = { - Filter01: { count: 1, selected: false }, - Filter02: { count: 1, selected: false }, - Filter03: { count: 1, selected: false }, - Filter04: { count: 1, selected: false }, - Filter05: { count: 1, selected: false }, - Filter06: { count: 1, selected: false }, - Filter07: { count: 1, selected: false }, - Filter08: { count: 1, selected: false }, - Filter09: { count: 1, selected: false }, - Filter10: { count: 1, selected: false }, - Filter11: { count: 1, selected: false }, - Filter12: { count: 1, selected: false }, - Filter13: { count: 1, selected: false }, -}; - -module("Integration | Component | header/facet-dropdown", function (hooks) { - setupRenderingTest(hooks); - - test("it toggles when the trigger is clicked", async function (assert) { - this.set("facets", SHORT_FACET_LIST); - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - assert.dom("[data-test-facet-dropdown-popover]").doesNotExist(); - await click("[data-test-facet-dropdown-trigger]"); - assert.dom("[data-test-facet-dropdown-popover]").exists("The dropdown is shown"); - }); - - test("it renders the facets correctly", async function (assert) { - this.set("facets", SHORT_FACET_LIST); - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - await click("[data-test-facet-dropdown-trigger]"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(1)") - .hasText("RFC 10", "Correct facet name and count"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(1) .flight-icon") - .hasStyle({ visibility: "hidden" }, "Unselected facets have no icon"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2)") - .hasText("PRD 5", "Correct facet name and count"); - assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2) .flight-icon") - .hasStyle({ visibility: "visible" }, "Selected facets have an icon"); - }); - - test("an input is shown when there are more than 12 facets", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - await click("[data-test-facet-dropdown-trigger]"); - assert.dom("[data-test-facet-dropdown-input]").exists("The input is shown"); - }); - - test("filtering works as expected", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - await click("[data-test-facet-dropdown-trigger]"); - - let firstItemSelector = "#facet-dropdown-menu-item-0"; - - assert.dom(firstItemSelector).hasText("Filter01 1"); - assert.dom("[data-test-facet-dropdown-menu-item]").exists({ count: 13 }); - await fillIn("[data-test-facet-dropdown-input]", "3"); - - assert - .dom("[data-test-facet-dropdown-menu-item]") - .exists({ count: 2 }, "The facets are filtered"); - assert - .dom(firstItemSelector) - .hasText( - "Filter03 1", - "The facet IDs are updated when the list is filtered to match the new order" - ); - - await fillIn("[data-test-facet-dropdown-input]", "foobar"); - - assert.dom("[data-test-facet-dropdown-menu]").doesNotExist(); - assert - .dom("[data-test-facet-dropdown-menu-empty-state]") - .exists('the "No matches" message is shown'); - }); - - test("popover trigger has keyboard support", async function (assert) { - this.set("facets", LONG_FACET_LIST); - await render(hbs` - {{! @glint-nocheck: not typesafe yet }} - - `); - - assert.dom("[data-test-facet-dropdown-popover]").doesNotExist(); - - await triggerKeyEvent( - "[data-test-facet-dropdown-trigger]", - "keydown", - "ArrowDown" - ); - - assert.dom("[data-test-facet-dropdown-popover]").exists("The dropdown is shown"); - let firstItemSelector = "#facet-dropdown-menu-item-0"; - - assert.dom(firstItemSelector).hasAttribute("aria-selected"); - assert - .dom("[data-test-facet-dropdown-menu]") - .hasAttribute("aria-activedescendant", "facet-dropdown-menu-item-0"); - }); -}); diff --git a/web/tests/integration/components/header/toolbar-test.ts b/web/tests/integration/components/header/toolbar-test.ts index f55dd87c6..3df0622d5 100644 --- a/web/tests/integration/components/header/toolbar-test.ts +++ b/web/tests/integration/components/header/toolbar-test.ts @@ -1,9 +1,8 @@ import { module, test, todo } from "qunit"; import { setupRenderingTest } from "ember-qunit"; -import { click, find, findAll, render } from "@ember/test-helpers"; +import { click, findAll, render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { FacetDropdownObjects } from "hermes/types/facets"; -import RouterService from "@ember/routing/router-service"; import { SortByLabel } from "hermes/components/header/toolbar"; const FACETS = { @@ -48,27 +47,28 @@ module("Integration | Component | header/toolbar", function (hooks) { assert.dom(".facets").exists(); assert - .dom("[data-test-facet-dropdown='sort']") + .dom("[data-test-header-sort-dropdown-trigger]") .exists("Sort-by dropdown is shown with facets unless explicitly hidden"); assert .dom(".facets [data-test-facet-dropdown-trigger]") .exists({ count: 4 }); assert - .dom('[data-test-facet-dropdown="sort"]') + .dom("[data-test-header-sort-dropdown-trigger]") .exists({ count: 1 }) .hasText(`Sort: ${SortByLabel.Newest}`); - await click( - `[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']` - ); + await click(`[data-test-header-sort-dropdown-trigger]`); + assert - .dom("[data-test-facet-dropdown-menu-item]:nth-child(2)") + .dom( + "[data-test-header-sort-by-dropdown] .x-dropdown-list-item:nth-child(2)" + ) .hasText("Oldest"); this.set("sortControlIsHidden", true); assert - .dom(".sort-by-dropdown") + .dom("[data-test-header-sort-by-dropdown]") .doesNotExist("Sort-by dropdown hides when sortByHidden is true"); }); @@ -101,9 +101,9 @@ module("Integration | Component | header/toolbar", function (hooks) { await click("[data-test-facet-dropdown-trigger='Status']"); assert.deepEqual( - findAll( - "[data-test-facet-dropdown-menu-item] [data-test-facet-dropdown-list-item-value]" - )?.map((el) => el.textContent?.trim()), + findAll(".x-dropdown-list-item-value")?.map((el) => + el.textContent?.trim() + ), ["Approved", "In-Review", "In Review", "Obsolete", "WIP"], "Unsupported statuses are filtered out" ); @@ -128,12 +128,12 @@ module("Integration | Component | header/toolbar", function (hooks) { `); assert - .dom(`[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']`) + .dom(`[data-test-header-sort-dropdown-trigger]`) .doesNotHaveAttribute("disabled"); - this.set("facets", {}); + this.set("facets", {}); assert - .dom(`[data-test-facet-dropdown-trigger='Sort: ${SortByLabel.Newest}']`) + .dom(`[data-test-header-sort-dropdown-trigger]`) .hasAttribute("disabled"); }); From e7b75c309241fca99c22cdff0f110a920870b045 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 2 May 2023 14:50:23 -0400 Subject: [PATCH 070/128] Improve styling --- web/app/components/header/facet-dropdown.hbs | 7 ++++++- web/app/components/header/toolbar.hbs | 3 ++- web/app/components/x/dropdown-list/index.hbs | 4 +++- web/app/styles/app.scss | 1 + web/app/styles/components/header/facet-dropdown.scss | 11 +++++++++++ 5 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 web/app/styles/components/header/facet-dropdown.scss diff --git a/web/app/components/header/facet-dropdown.hbs b/web/app/components/header/facet-dropdown.hbs index 5fe7ccec3..b6f9b4518 100644 --- a/web/app/components/header/facet-dropdown.hbs +++ b/web/app/components/header/facet-dropdown.hbs @@ -1,6 +1,11 @@ {{! @glint-nocheck: not typesafe yet }} - + <:anchor as |dd|> diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index bb1031cee..7949942ae 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -14,12 +14,13 @@ @label="Type" @facets={{@facets.docType}} @disabled={{not @facets.docType}} + class="" /> diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 618129c11..29e5e9283 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -24,6 +24,7 @@ @use "components/notification"; @use "components/sidebar"; @use "components/hds-badge"; +@use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; @use "hashicorp/product-badge"; diff --git a/web/app/styles/components/header/facet-dropdown.scss b/web/app/styles/components/header/facet-dropdown.scss new file mode 100644 index 000000000..aa57a43dc --- /dev/null +++ b/web/app/styles/components/header/facet-dropdown.scss @@ -0,0 +1,11 @@ +.facet-dropdown-popover { + @apply min-w-[175px]; + + &.medium:not(.has-input) { + @apply w-[240px]; + } + + &.has-input { + @apply w-[320px]; + } +} From 485e2cb77dfa71aec72a8fa2893b58bf8f0603f3 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Sat, 13 May 2023 18:44:03 -0400 Subject: [PATCH 071/128] Basic search keyboardFunctions done --- web/app/components/header/search.hbs | 114 ++++++++++++------ web/app/components/header/search.ts | 38 ++++-- web/app/components/x/dropdown-list/index.hbs | 16 +++ web/app/components/x/dropdown-list/index.ts | 18 ++- web/app/components/x/dropdown-list/item.hbs | 1 + web/app/components/x/dropdown-list/items.hbs | 1 + .../components/x/dropdown-list/link-to.hbs | 1 + web/app/components/x/dropdown-list/link-to.ts | 2 +- 8 files changed, 144 insertions(+), 47 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 76fcdc36b..88b08fa68 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -2,41 +2,89 @@ {{on-document "keydown" this.onKeydown}}
    - - - {{#unless this.query}} - - ⌘K - - {{/unless}} - {{#if this.query}} - + {{! TODO: add array support to dropdownList }} + + <:anchor as |dd|> + + {{#unless this.query}} + + ⌘K + + {{/unless}} + + <:item as |dd|> + {{#if (eq dd.value "viewAllResultsObject")}} + + + View all results for "{{this.query}}" + + {{else}} + + +
    +

    + {{dd.attrs.title}} +

    + + {{#if (not (is-empty dd.attrs.docNumber))}} + + {{dd.attrs.docNumber}} + + {{/if}} + + + + {{#if dd.attrs._snippetResult.content.value}} + + {{/if}} +
    +
    + {{/if}} + +
    + + {{!-- {{#if this.bestMatches}}
    - - - View all results for "{{this.query}}" - +
    {{/if}} -
    - {{/if}} -
    + --}}
    diff --git a/web/app/components/header/search.ts b/web/app/components/header/search.ts index 6f35ab5a0..94d9c572e 100644 --- a/web/app/components/header/search.ts +++ b/web/app/components/header/search.ts @@ -20,14 +20,34 @@ interface BasicDropdownAPI { }; } +interface SearchResultObjects { + [key: string]: unknown | HermesDocumentObjects; +} + +interface HermesDocumentObjects { + [key: string]: HermesDocument; +} + export default class Search extends Component { @service declare algolia: AlgoliaService; @service declare router: RouterService; @tracked protected searchInput: HTMLInputElement | null = null; - @tracked protected bestMatches: HermesDocument[] = []; + @tracked protected _bestMatches: HermesDocument[] = []; @tracked protected query: string = ""; + get bestMatches(): SearchResultObjects { + return this._bestMatches.reduce( + (acc, doc) => { + acc[doc.objectID] = doc; + return acc; + }, + { + viewAllResultsObject: {}, + } as SearchResultObjects + ); + } + @action protected registerInput(element: HTMLInputElement): void { this.searchInput = element; } @@ -45,9 +65,9 @@ export default class Search extends Component { * Uses mousedown instead of click to get ahead of the focusin event. * This allows users to click the search input to dismiss the dropdown. */ - @action protected maybeCloseDropdown(dd: BasicDropdownAPI): void { - if (dd.isOpen) { - dd.actions.close(); + @action protected maybeCloseDropdown(dd: any): void { + if (dd.contentIsShown) { + dd.hideContent(); } } @@ -59,7 +79,7 @@ export default class Search extends Component { } protected search = restartableTask( - async (dd: BasicDropdownAPI, inputEvent: InputEvent): Promise => { + async (dd: any, inputEvent: InputEvent): Promise => { let input = inputEvent.target; assert( @@ -76,14 +96,16 @@ export default class Search extends Component { const response = await this.algolia.search.perform(this.query, params); if (response) { - this.bestMatches = response.hits as HermesDocument[]; + this._bestMatches = response.hits as HermesDocument[]; } } // Reopen the dropdown if it was closed on mousedown - if (!dd.isOpen) { - dd.actions.open(); + if (!dd.contentIsShown) { + dd.showContent(); } + dd.resetFocusedItemIndex(); + dd.scheduleAssignMenuItemIDs(); } ); } diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index cb6b21085..dc19f4ca9 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck: not typesafe yet }} {{! Marked up with guidance from: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ @@ -50,7 +51,22 @@ f.contentID ) ) + ariaControls=(concat + "x-dropdown-list-" + (if this.inputIsShown "container" "items") + "-" + f.contentID + ) + resetFocusedItemIndex=this.resetFocusedItemIndex + scheduleAssignMenuItemIDs=this.scheduleAssignMenuItemIDs + registerAnchor=f.registerAnchor contentIsShown=f.contentIsShown + toggleContent=f.toggleContent + onTriggerKeydown=(fn + this.onTriggerKeydown f.contentIsShown f.showContent + ) + hideContent=f.hideContent + showContent=f.showContent ) to="anchor" }} diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index b9f9bfaea..b0d8d35cc 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -115,7 +115,7 @@ export default class XDropdownListComponent extends Component< @action onDestroy() { this.query = ""; this._filteredItems = null; - this.focusedItemIndex = -1; + this.resetFocusedItemIndex(); } /** @@ -262,13 +262,17 @@ export default class XDropdownListComponent extends Component< this.scrollContainer.scrollTop = itemTop; } } + + @action protected resetFocusedItemIndex() { + this.focusedItemIndex = -1; + } /** * The action run when the user types in the input. * Filters the facets shown in the dropdown and schedules * the menu items to be assigned their new IDs. */ protected onInput = restartableTask(async (inputEvent: InputEvent) => { - this.focusedItemIndex = -1; + this.resetFocusedItemIndex(); let shownItems: any = {}; let { items } = this.args; @@ -281,12 +285,18 @@ export default class XDropdownListComponent extends Component< } this._filteredItems = shownItems; + this.scheduleAssignMenuItemIDs(); + }); + @action protected scheduleAssignMenuItemIDs() { schedule("afterRender", () => { - assert("onInput expects a _scrollContainer", this._scrollContainer); + assert( + "scheduleAssignMenuItemIDs expects a _scrollContainer", + this._scrollContainer + ); this.assignMenuItemIDs( this._scrollContainer.querySelectorAll(`[role=${this.listItemRole}]`) ); }); - }); + } } diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index 208506283..6523c82e4 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck: not typesafe yet }}
  • {{yield (hash diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index e884156c6..c85fdd11d 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck: not typesafe yet }} {{! Listen for ArrowUp/ArrowDown/Enter }} {{on-document "keydown" this.maybeKeyboardNavigate}} diff --git a/web/app/components/x/dropdown-list/link-to.hbs b/web/app/components/x/dropdown-list/link-to.hbs index 472dd6286..f55414121 100644 --- a/web/app/components/x/dropdown-list/link-to.hbs +++ b/web/app/components/x/dropdown-list/link-to.hbs @@ -10,6 +10,7 @@ tabindex="-1" aria-checked={{@isAriaChecked}} @route={{@route}} + @model={{@model}} @query={{or @query (hash)}} class="x-dropdown-list-item-link {{if @isAriaSelected 'is-aria-selected'}}" ...attributes diff --git a/web/app/components/x/dropdown-list/link-to.ts b/web/app/components/x/dropdown-list/link-to.ts index 5705ba868..74f9d05aa 100644 --- a/web/app/components/x/dropdown-list/link-to.ts +++ b/web/app/components/x/dropdown-list/link-to.ts @@ -1,7 +1,7 @@ import Component from "@glimmer/component"; interface XDropdownListLinkToComponentSignature { - Element: HTMLButtonElement; + Element: HTMLAnchorElement; Args: { registerElement: () => void; focusMouseTarget: () => void; From aa1e98e1a7a1b48361042e0c4d9b8b14011bc994 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Sun, 14 May 2023 20:09:41 -0400 Subject: [PATCH 072/128] Improved semantics of header --- web/app/components/header/search.hbs | 26 ++++++++++++++------ web/app/components/x/dropdown-list/index.hbs | 8 ++++++ web/app/components/x/dropdown-list/item.hbs | 1 + web/app/components/x/dropdown-list/items.hbs | 1 + 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 88b08fa68..83d844334 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -29,15 +29,27 @@ {{/unless}} + <:header> +
    + {{! content is placed by in-element }} +
    + <:item as |dd|> {{#if (eq dd.value "viewAllResultsObject")}} - - - View all results for "{{this.query}}" - + {{#in-element + (html-element "#hermes-search-popover-header") + insertBefore=null + }} +
    + + + View all results for "{{this.query}}" + +
    + {{/in-element}} {{else}} + {{#if (has-block "header")}} + {{yield to="header"}} + {{/if}} + + + {{#if (has-block "footer")}} + {{yield to="footer"}} + {{/if}}
  • diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index 6523c82e4..7ffd90547 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -20,6 +20,7 @@ focusMouseTarget=this.focusMouseTarget onClick=this.onClick ) + contentID=@contentID value=@value attrs=@attributes selected=@selected diff --git a/web/app/components/x/dropdown-list/items.hbs b/web/app/components/x/dropdown-list/items.hbs index c85fdd11d..4498f3834 100644 --- a/web/app/components/x/dropdown-list/items.hbs +++ b/web/app/components/x/dropdown-list/items.hbs @@ -19,6 +19,7 @@ {{#each-in @shownItems as |item attrs|}} Date: Mon, 15 May 2023 10:10:26 -0400 Subject: [PATCH 073/128] Improve model/query args; add Glint boilerplate --- .../components/x/dropdown-list/link-to.hbs | 6 ++-- web/app/components/x/dropdown-list/link-to.ts | 36 +++++++++++++++++-- web/types/glint/index.d.ts | 7 ++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 web/types/glint/index.d.ts diff --git a/web/app/components/x/dropdown-list/link-to.hbs b/web/app/components/x/dropdown-list/link-to.hbs index f55414121..9b462b9d7 100644 --- a/web/app/components/x/dropdown-list/link-to.hbs +++ b/web/app/components/x/dropdown-list/link-to.hbs @@ -1,5 +1,3 @@ -{{! TODO: add @model support }} - diff --git a/web/app/components/x/dropdown-list/link-to.ts b/web/app/components/x/dropdown-list/link-to.ts index 74f9d05aa..8d537e18e 100644 --- a/web/app/components/x/dropdown-list/link-to.ts +++ b/web/app/components/x/dropdown-list/link-to.ts @@ -12,8 +12,40 @@ interface XDropdownListLinkToComponentSignature { isAriaSelected: boolean; isAriaChecked: boolean; route: string; - query?: unknown; + query?: string; + model?: unknown; + models?: unknown[]; }; + Blocks: { + default: []; + } } -export default class XDropdownListLinkToComponent extends Component {} +export default class XDropdownListLinkToComponent extends Component { + /** + * The models, if any, to pass to the route. + * Allows the component to support all model scenarios without + * hitting internal Ember assertions. + */ + protected get models() { + if (this.args.models) { + return this.args.models; + } else { + return this.args.model ? [this.args.model] : []; + } + } + /** + * The query, if any, to pass to the route. + * Workaround for https://github.com/emberjs/ember.js/issues/19693 + * Can be removed when we upgrade to Ember 3.28+ + */ + protected get query() { + return this.args.query || {}; + } +} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "x/dropdown-list/link-to": typeof XDropdownListLinkToComponent; + } +} diff --git a/web/types/glint/index.d.ts b/web/types/glint/index.d.ts new file mode 100644 index 000000000..17b1e3062 --- /dev/null +++ b/web/types/glint/index.d.ts @@ -0,0 +1,7 @@ +import "@glint/environment-ember-loose"; + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "did-insert": typeof import("@gavant/glint-template-types/types/ember-render-modifiers/did-insert").default; + } +} From f6114d1757f3fc7c7975034d8c7dbaa56f61d052 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 10:16:26 -0400 Subject: [PATCH 074/128] Contextual "Best matches" header --- web/app/components/header/search.hbs | 3 +++ web/app/components/header/search.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 83d844334..d9846b2f9 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -33,6 +33,9 @@
    {{! content is placed by in-element }}
    + {{#if this.bestMatchesHeaderIsShown}} +
    Best matches
    + {{/if}} <:item as |dd|> {{#if (eq dd.value "viewAllResultsObject")}} diff --git a/web/app/components/header/search.ts b/web/app/components/header/search.ts index 94d9c572e..6292fd532 100644 --- a/web/app/components/header/search.ts +++ b/web/app/components/header/search.ts @@ -36,6 +36,10 @@ export default class Search extends Component { @tracked protected _bestMatches: HermesDocument[] = []; @tracked protected query: string = ""; + get bestMatchesHeaderIsShown(): boolean { + return Object.keys(this.bestMatches).length > 1; + } + get bestMatches(): SearchResultObjects { return this._bestMatches.reduce( (acc, doc) => { From aeef9b6bc9ca21d6c4c717d344d71a93f345618a Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 11:31:37 -0400 Subject: [PATCH 075/128] Improve popover styles --- web/app/components/floating-u-i/index.ts | 1 + web/app/components/header/search.hbs | 46 ++++++++++--------- web/app/components/x/dropdown-list/index.hbs | 3 +- web/app/styles/app.scss | 1 + web/app/styles/components/header/search.scss | 24 ++++++++++ .../styles/components/x/dropdown/list.scss | 2 +- 6 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 web/app/styles/components/header/search.scss diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts index 3c70616f1..2393d70a8 100644 --- a/web/app/components/floating-u-i/index.ts +++ b/web/app/components/floating-u-i/index.ts @@ -6,6 +6,7 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; interface FloatingUIComponentSignature { + Element: HTMLDivElement; Args: { renderOut?: boolean; placement?: Placement; diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index d9846b2f9..b69f4daba 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -1,9 +1,14 @@ {{! @glint-nocheck: not typesafe yet }} {{on-document "keydown" this.onKeydown}}
    -
    + {{! TODO: add array support to dropdownList }} - + <:anchor as |dd|> <:header>
    - {{! content is placed by in-element }} + {{! content is placed here by the in-element helper below }}
    {{#if this.bestMatchesHeaderIsShown}} -
    Best matches
    +
    +
    + Best matches +
    +
    {{/if}} <:item as |dd|> @@ -43,21 +54,20 @@ (html-element "#hermes-search-popover-header") insertBefore=null }} -
    - - - View all results for "{{this.query}}" - -
    + + + View all results for "{{this.query}}" + {{/in-element}} {{else}}

    {{dd.attrs.title}}

    - {{#if (not (is-empty dd.attrs.docNumber))}} - - {{dd.attrs.docNumber}} - - {{/if}} - <:anchor as |f|> {{yield diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 29e5e9283..7204817a9 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -15,6 +15,7 @@ @use "components/template-card"; @use "components/header/active-filter-list"; @use "components/header/active-filter-list-item"; +@use "components/header/search"; @use "components/doc/tile-list"; @use "components/doc/thumbnail"; @use "components/doc/folder-affordance"; diff --git a/web/app/styles/components/header/search.scss b/web/app/styles/components/header/search.scss new file mode 100644 index 000000000..e40bd1ff9 --- /dev/null +++ b/web/app/styles/components/header/search.scss @@ -0,0 +1,24 @@ +.x-dropdown-list.search-popover { + @apply w-full max-h-[none] max-w-none min-w-[400px]; + + &.no-best-matches { + .x-dropdown-list-items { + @apply hidden; + } + } + .x-dropdown-list-item-link { + @apply items-start; + + &.all-results-link { + @apply pt-2.5 pb-[9px] items-center; + } + + &.is-aria-selected { + @apply bg-color-surface-interactive-hover text-inherit; + } + + .flight-icon { + @apply text-inherit; + } + } +} diff --git a/web/app/styles/components/x/dropdown/list.scss b/web/app/styles/components/x/dropdown/list.scss index 02b8b6d79..31f16effd 100644 --- a/web/app/styles/components/x/dropdown/list.scss +++ b/web/app/styles/components/x/dropdown/list.scss @@ -7,7 +7,7 @@ } .x-dropdown-list-container { - @apply flex flex-col overflow-hidden; + @apply flex flex-col overflow-hidden rounded-md; } .x-dropdown-list-input-container { From 7caa6eed152e8cbadb344b031e2d44145919d557 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 20:12:05 -0400 Subject: [PATCH 076/128] UX Improvements - Add floatingUIOffset - Improved empty-search handling - Improved keyboard handling - Improve productMatch suggestion - Improve enter-to-submit function --- web/app/components/floating-u-i/content.ts | 6 +- web/app/components/floating-u-i/index.hbs | 2 +- web/app/components/header/search.hbs | 61 ++++++--- web/app/components/header/search.ts | 124 ++++++++++++++----- web/app/components/x/dropdown-list/index.hbs | 3 +- web/app/styles/components/header/search.scss | 7 +- 6 files changed, 151 insertions(+), 52 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index cb79314e0..e0b68e9a0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -1,6 +1,7 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { + OffsetOptions, Placement, autoUpdate, computePosition, @@ -18,6 +19,7 @@ interface FloatingUIContentSignature { id: string; placement?: Placement; renderOut?: boolean; + offset?: OffsetOptions; }; } @@ -30,6 +32,8 @@ export default class FloatingUIContent extends Component { this.content.setAttribute("data-floating-ui-placement", placement); diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index 4c327d916..4d1eb30bb 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -12,9 +12,9 @@ to="anchor" }} - {{#if this.contentIsShown}} - - {{! TODO: add array support to dropdownList }} + <:item as |dd|> - {{#if (eq dd.value "viewAllResultsObject")}} - {{#in-element - (html-element "#hermes-search-popover-header") - insertBefore=null - }} - - - View all results for "{{this.query}}" - - {{/in-element}} + {{#if + (or + (eq dd.value "viewAllResultsObject") + (eq dd.value "productAreaMatch") + ) + }} + {{#unless this.searchInputIsEmpty}} + {{#in-element + (html-element "#hermes-search-popover-header") + insertBefore=null + }} + {{#if (eq dd.value "viewAllResultsObject")}} + + + View all results for "{{this.query}}" + + {{else}} + + {{log dd.attrs.product}} + + View all {{dd.attrs.product}} documents + + {{/if}} + {{/in-element}} + {{/unless}} {{else}} void; - open: () => void; - toggle: () => void; - reposition: () => void; - }; -} +import { OffsetOptions } from "@floating-ui/dom"; +import ConfigService from "hermes/services/config"; +import { next } from "@ember/runloop"; interface SearchResultObjects { [key: string]: unknown | HermesDocumentObjects; @@ -28,18 +19,38 @@ interface HermesDocumentObjects { [key: string]: HermesDocument; } +const POPOVER_CROSS_AXIS_OFFSET = 3; +const POPOVER_BORDER_WIDTH = 1; + export default class Search extends Component { + @service("config") declare configSvc: ConfigService; @service declare algolia: AlgoliaService; @service declare router: RouterService; @tracked protected searchInput: HTMLInputElement | null = null; + @tracked protected searchInputIsEmpty = true; @tracked protected _bestMatches: HermesDocument[] = []; + @tracked protected _productAreaMatch: HermesDocument | null = null; + @tracked protected viewAllResultsLink: HTMLAnchorElement | null = null; @tracked protected query: string = ""; get bestMatchesHeaderIsShown(): boolean { return Object.keys(this.bestMatches).length > 1; } + get dropdownListStyle(): string { + return `width: calc(100% + ${ + POPOVER_BORDER_WIDTH + POPOVER_CROSS_AXIS_OFFSET + }px)`; + } + + get popoverOffset(): OffsetOptions { + return { + mainAxis: 0, + crossAxis: POPOVER_CROSS_AXIS_OFFSET, + }; + } + get bestMatches(): SearchResultObjects { return this._bestMatches.reduce( (acc, doc) => { @@ -48,15 +59,37 @@ export default class Search extends Component { }, { viewAllResultsObject: {}, + ...(this._productAreaMatch && { + productAreaMatch: this._productAreaMatch, + }), } as SearchResultObjects ); } + @action onInputKeydown(dd: any, e: KeyboardEvent): void { + if (e.key === "ArrowUp" || e.key === "ArrowDown") { + if (!this.query.length) { + e.preventDefault(); + return; + } + } + + if (e.key === "Enter" && dd.focusedItemIndex === -1) { + if (!dd.selected) { + e.preventDefault(); + this.viewAllResults(); + dd.hideContent(); + } + } + + dd.onTriggerKeydown(dd.contentIsShown, dd.showContent, e); + } + @action protected registerInput(element: HTMLInputElement): void { this.searchInput = element; } - @action protected onKeydown(e: KeyboardEvent): void { + @action protected onDocumentKeydown(e: KeyboardEvent): void { if (e.metaKey && e.key === "k") { e.preventDefault(); assert("searchInput is expected", this.searchInput); @@ -64,6 +97,10 @@ export default class Search extends Component { } } + @action registerViewAllResultsLink(e: HTMLAnchorElement) { + this.viewAllResultsLink = e; + } + /** * Checks whether the dropdown is open and closes it if it is. * Uses mousedown instead of click to get ahead of the focusin event. @@ -75,11 +112,15 @@ export default class Search extends Component { } } - @action protected goToResults(ev: Event): void { - ev.preventDefault(); - this.router.transitionTo("authenticated.results", { - queryParams: { q: this.query }, - }); + @action protected maybeOpenDropdown(dd: any): void { + if (!dd.contentIsShown && this.query.length) { + dd.showContent(); + } + } + + @action protected viewAllResults(): void { + assert("viewAllResultsLink is expected", this.viewAllResultsLink); + this.viewAllResultsLink.click(); } protected search = restartableTask( @@ -93,23 +134,50 @@ export default class Search extends Component { this.query = input.value; - if (this.query) { - const params = { - hitsPerPage: 5, - }; - const response = await this.algolia.search.perform(this.query, params); + if (this.query.length) { + this.searchInputIsEmpty = false; - if (response) { - this._bestMatches = response.hits as HermesDocument[]; - } + const productSearch = this.algolia.searchIndex.perform( + this.configSvc.config.algolia_docs_index_name + "_product_areas", + this.query, + { + hitsPerPage: 1, + } + ); + + const docSearch = this.algolia.search.perform(this.query, { + hitsPerPage: 5, + }); + + let algoliaResults = await Promise.all([productSearch, docSearch]).then( + (values) => values + ); + + let [productAreas, docs] = algoliaResults; + + this._bestMatches = docs ? (docs.hits as HermesDocument[]) : []; + this._productAreaMatch = productAreas + ? (productAreas.hits[0] as HermesDocument) + : null; + } else { + this._productAreaMatch = null; + this.searchInputIsEmpty = true; + dd.hideContent(); + this._bestMatches = []; } // Reopen the dropdown if it was closed on mousedown if (!dd.contentIsShown) { dd.showContent(); } - dd.resetFocusedItemIndex(); - dd.scheduleAssignMenuItemIDs(); + + // Although the `dd.scheduleAssignMenuItemIDs` method runs `afterRender`, + // it doesn't provide enough time for `in-element` to update. + // Therefore, we wait for the next run loop when the DOM is updated. + next(() => { + dd.resetFocusedItemIndex(); + dd.scheduleAssignMenuItemIDs(); + }); } ); } diff --git a/web/app/components/x/dropdown-list/index.hbs b/web/app/components/x/dropdown-list/index.hbs index 84bc5695b..61a2c11c7 100644 --- a/web/app/components/x/dropdown-list/index.hbs +++ b/web/app/components/x/dropdown-list/index.hbs @@ -10,6 +10,7 @@ {{#if this.inputIsShown}}
    diff --git a/web/app/styles/components/header/search.scss b/web/app/styles/components/header/search.scss index e40bd1ff9..c03ad7a6d 100644 --- a/web/app/styles/components/header/search.scss +++ b/web/app/styles/components/header/search.scss @@ -1,5 +1,6 @@ .x-dropdown-list.search-popover { - @apply w-full max-h-[none] max-w-none min-w-[400px]; + // width calculated in the component class + @apply max-h-[none] min-w-[420px] max-w-[none]; &.no-best-matches { .x-dropdown-list-items { @@ -9,8 +10,8 @@ .x-dropdown-list-item-link { @apply items-start; - &.all-results-link { - @apply pt-2.5 pb-[9px] items-center; + &.search-popover-header-link { + @apply pt-2.5 pb-[10px] items-center; } &.is-aria-selected { From 9d52041907260c7542d3d9be6515037f170131a4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 20:15:45 -0400 Subject: [PATCH 077/128] Update product/area link --- web/app/components/header/search.hbs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 321db3be0..a8506fa0a 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -79,11 +79,16 @@ class="search-popover-header-link border-t border-t-color-border-primary" > {{log dd.attrs.product}} - - View all {{dd.attrs.product}} documents + + + View all + + documents + {{/if}} {{/in-element}} From a35a1b70ae9f53756485dc8ac98ca26754f13650 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 20:45:31 -0400 Subject: [PATCH 078/128] Add support for offset --- web/app/components/floating-u-i/content.ts | 6 +- web/app/components/floating-u-i/index.hbs | 2 +- .../components/floating-u-i/content-test.ts | 77 +++++++++++++++++-- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index cb79314e0..e0b68e9a0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -1,6 +1,7 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { + OffsetOptions, Placement, autoUpdate, computePosition, @@ -18,6 +19,7 @@ interface FloatingUIContentSignature { id: string; placement?: Placement; renderOut?: boolean; + offset?: OffsetOptions; }; } @@ -30,6 +32,8 @@ export default class FloatingUIContent extends Component { this.content.setAttribute("data-floating-ui-placement", placement); diff --git a/web/app/components/floating-u-i/index.hbs b/web/app/components/floating-u-i/index.hbs index 4c327d916..4d1eb30bb 100644 --- a/web/app/components/floating-u-i/index.hbs +++ b/web/app/components/floating-u-i/index.hbs @@ -12,9 +12,9 @@ to="anchor" }} - {{#if this.contentIsShown}} +
    +
    + Attach +
    + + Content + +
    +
    + `); + + let anchor = htmlElement(".anchor"); + let content = htmlElement(".hermes-floating-ui-content"); + let contentWidth = content.offsetWidth; + let contentRight = content.offsetLeft + contentWidth; + let anchorLeft = anchor.offsetLeft; + + assert.equal( + contentRight, + anchorLeft - DEFAULT_CONTENT_OFFSET, + "content is offset to the left of the anchor" + ); + + // Clear and set the offset to 10 + this.clearRender(); + this.set("offset", 10); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} +
    +
    +
    + Attach +
    + + Content + +
    +
    + `); + + anchor = htmlElement(".anchor"); + content = htmlElement(".hermes-floating-ui-content"); + contentWidth = content.offsetWidth; + contentRight = content.offsetLeft + contentWidth; + anchorLeft = anchor.offsetLeft; + + assert.equal( + contentRight, + anchorLeft - 10, + "content is offset by the passed-in value" + ); + }); + todo("it runs a cleanup function on teardown", async function (assert) { assert.ok(false); }); From 99bed689b2fd5826f0f39436eaf69c6f4d97edec Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 15 May 2023 20:49:17 -0400 Subject: [PATCH 079/128] Removed commented-out code --- web/app/components/header/search.hbs | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index a8506fa0a..6f14871de 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -127,60 +127,5 @@ {{/if}}
    - - {{!-- - {{#if this.bestMatches}} -
    - -
    -
    - {{#let (get (get this.bestMatches 0) "product") as |product|}} - - - View all - - documents - - - {{/let}} -
    -
    - Best matches - {{#each this.bestMatches as |match|}} - - {{/each}} -
    - {{else}} -
    -
    - No results found for "{{this.query}}" -
    -
    - {{/if}} -
    --}}
    From b9348c9f2ae4b486ba248921dac8880f0d05e216 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 11:01:06 -0400 Subject: [PATCH 080/128] Add and test LinkTo helpers --- web/app/helpers/maybe-query.ts | 11 +++ web/app/helpers/model-or-models.ts | 36 +++++++++ .../helpers/model-or-models-test.ts | 78 +++++++++++++++++++ web/types/glint/index.d.ts | 1 + 4 files changed, 126 insertions(+) create mode 100644 web/app/helpers/maybe-query.ts create mode 100644 web/app/helpers/model-or-models.ts create mode 100644 web/tests/integration/helpers/model-or-models-test.ts create mode 100644 web/types/glint/index.d.ts diff --git a/web/app/helpers/maybe-query.ts b/web/app/helpers/maybe-query.ts new file mode 100644 index 000000000..85c91edb2 --- /dev/null +++ b/web/app/helpers/maybe-query.ts @@ -0,0 +1,11 @@ +import { helper } from "@ember/component/helper"; + +/** + * Supplies an empty object if no is query provided. + * Avoids errors when passing empty `@query` values to LinkTos. + * Workaround for https://github.com/emberjs/ember.js/issues/19693 + * Can be removed when we upgrade to Ember 3.28+ + */ +export default helper(([query]: [unknown | undefined | null]) => { + return query ? query : {}; +}); diff --git a/web/app/helpers/model-or-models.ts b/web/app/helpers/model-or-models.ts new file mode 100644 index 000000000..bbf0773f0 --- /dev/null +++ b/web/app/helpers/model-or-models.ts @@ -0,0 +1,36 @@ +import { helper } from "@ember/component/helper"; +import { assert } from "@ember/debug"; + +export interface ModelOrModelsSignature { + Args: { + Positional: [unknown | undefined, Array | undefined]; + }; + Return: Array; +} + +/** + * Returns the model or models based on the arguments passed. + * Allows a LinkTo-based component to support all model scenarios (including no models) + * without hitting internal assertions. + */ +const modelOrModelsHelper = helper( + ([model, models]: [unknown | undefined, Array | undefined]) => { + assert( + "You can't pass both `@model` and `@models` to a LinkTo", + !model || !models + ); + if (models) { + return models; + } else { + return model ? [model] : []; + } + } +); + +export default modelOrModelsHelper; + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "model-or-models": typeof modelOrModelsHelper; + } +} diff --git a/web/tests/integration/helpers/model-or-models-test.ts b/web/tests/integration/helpers/model-or-models-test.ts new file mode 100644 index 000000000..765df196f --- /dev/null +++ b/web/tests/integration/helpers/model-or-models-test.ts @@ -0,0 +1,78 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { TestContext, find, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; + +interface TestModel { + id: number; + name: string; +} + +interface ModelOrModelsTestContext extends TestContext { + model?: TestModel; + models?: TestModel[]; +} + +module("Integration | Helper | model-or-models", function (hooks) { + setupRenderingTest(hooks); + + test("it accepts a single model", async function (this: ModelOrModelsTestContext, assert) { + this.set("model", { id: 1, name: "foo" }); + + await render(hbs` + + Click me + + `); + + assert.equal( + find("a")?.getAttribute("href"), + "/document/1", + "it renders the correct href" + ); + }); + + test("it accepts an array of models", async function (this: ModelOrModelsTestContext, assert) { + this.set("models", [ + { id: 1, name: "foo" }, + { id: 2, name: "bar" }, + ]); + + /** + * LinkTos, even in testing scenarios, require valid routes and models, + * and since our app has no multi-model routes, we can't test this + * using the href attribute. (Ember drops the `href` attribute if + * the models are invalid.) So we use a loop instead. + */ + await render(hbs` +
    + {{#each (model-or-models this.model this.models) as |model|}} + {{! @glint-nocheck }} + {{model.name}} + {{/each}} +
    + `); + + assert.dom("div").hasText("foo bar", "it renders the correct models"); + }); + + test("it handles the no-model scenario", async function (this: ModelOrModelsTestContext, assert) { + await render(hbs` + + Click me + + `); + + assert.equal( + find("a")?.getAttribute("href"), + "/dashboard", + "it renders the correct href" + ); + }); +}); diff --git a/web/types/glint/index.d.ts b/web/types/glint/index.d.ts new file mode 100644 index 000000000..bb286ee63 --- /dev/null +++ b/web/types/glint/index.d.ts @@ -0,0 +1 @@ +import "@glint/environment-ember-loose"; From 86f624b3c8212a59fc5e3af36e6680049f169206 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 11:46:02 -0400 Subject: [PATCH 081/128] Tweak types, Glint boilerplate --- .../components/x/dropdown-list/link-to.hbs | 6 +-- web/app/components/x/dropdown-list/link-to.ts | 15 +++++- web/app/helpers/maybe-query.ts | 23 ++++++++-- web/app/helpers/model-or-models.ts | 4 +- .../integration/helpers/maybe-query-test.ts | 46 +++++++++++++++++++ .../helpers/model-or-models-test.ts | 2 +- 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 web/tests/integration/helpers/maybe-query-test.ts diff --git a/web/app/components/x/dropdown-list/link-to.hbs b/web/app/components/x/dropdown-list/link-to.hbs index 472dd6286..7ba9564c2 100644 --- a/web/app/components/x/dropdown-list/link-to.hbs +++ b/web/app/components/x/dropdown-list/link-to.hbs @@ -1,7 +1,6 @@ -{{! TODO: add @model support }} - diff --git a/web/app/components/x/dropdown-list/link-to.ts b/web/app/components/x/dropdown-list/link-to.ts index 5705ba868..a33e8ddaf 100644 --- a/web/app/components/x/dropdown-list/link-to.ts +++ b/web/app/components/x/dropdown-list/link-to.ts @@ -1,7 +1,7 @@ import Component from "@glimmer/component"; interface XDropdownListLinkToComponentSignature { - Element: HTMLButtonElement; + Element: HTMLAnchorElement; Args: { registerElement: () => void; focusMouseTarget: () => void; @@ -12,8 +12,19 @@ interface XDropdownListLinkToComponentSignature { isAriaSelected: boolean; isAriaChecked: boolean; route: string; - query?: unknown; + query?: Record; + model?: unknown; + models?: unknown[]; + }; + Blocks: { + default: []; }; } export default class XDropdownListLinkToComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "x/dropdown-list/link-to": typeof XDropdownListLinkToComponent; + } +} diff --git a/web/app/helpers/maybe-query.ts b/web/app/helpers/maybe-query.ts index 85c91edb2..ef594b68b 100644 --- a/web/app/helpers/maybe-query.ts +++ b/web/app/helpers/maybe-query.ts @@ -1,11 +1,28 @@ import { helper } from "@ember/component/helper"; +export interface MaybeQuerySignature { + Args: { + Positional: [Record | undefined]; + }; + Return: Record | {}; +} + /** * Supplies an empty object if no is query provided. * Avoids errors when passing empty `@query` values to LinkTos. * Workaround for https://github.com/emberjs/ember.js/issues/19693 * Can be removed when we upgrade to Ember 3.28+ */ -export default helper(([query]: [unknown | undefined | null]) => { - return query ? query : {}; -}); +const maybeQueryHelper = helper( + ([query]: [unknown | undefined]) => { + return query ? query : {}; + } +); + +export default maybeQueryHelper; + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "maybe-query": typeof maybeQueryHelper; + } +} diff --git a/web/app/helpers/model-or-models.ts b/web/app/helpers/model-or-models.ts index bbf0773f0..84c9b3456 100644 --- a/web/app/helpers/model-or-models.ts +++ b/web/app/helpers/model-or-models.ts @@ -3,9 +3,9 @@ import { assert } from "@ember/debug"; export interface ModelOrModelsSignature { Args: { - Positional: [unknown | undefined, Array | undefined]; + Positional: [unknown | undefined, unknown[] | undefined]; }; - Return: Array; + Return: unknown[]; } /** diff --git a/web/tests/integration/helpers/maybe-query-test.ts b/web/tests/integration/helpers/maybe-query-test.ts new file mode 100644 index 000000000..d1f53aa90 --- /dev/null +++ b/web/tests/integration/helpers/maybe-query-test.ts @@ -0,0 +1,46 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { TestContext, find, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; + +interface MaybeQueryTestContext extends TestContext { + query?: Record; +} + +module("Integration | Helper | maybe-query", function (hooks) { + setupRenderingTest(hooks); + + test("it accepts a valid query object", async function (this: MaybeQueryTestContext, assert) { + this.query = { product: ["waypoint"] }; + + await render(hbs` + + Link + + `); + + assert.equal( + find("a")?.getAttribute("href"), + "/all?product=%5B%22waypoint%22%5D", + "the passed-in query is used" + ); + }); + + test("it accepts an undefined query", async function (this: MaybeQueryTestContext, assert) { + this.query = undefined; + + await render(hbs` + + Link + + `); + + assert.equal(find("a")?.getAttribute("href"), "/all"); + }); +}); diff --git a/web/tests/integration/helpers/model-or-models-test.ts b/web/tests/integration/helpers/model-or-models-test.ts index 765df196f..6aa22432f 100644 --- a/web/tests/integration/helpers/model-or-models-test.ts +++ b/web/tests/integration/helpers/model-or-models-test.ts @@ -50,7 +50,7 @@ module("Integration | Helper | model-or-models", function (hooks) { await render(hbs`
    {{#each (model-or-models this.model this.models) as |model|}} - {{! @glint-nocheck }} + {{! @glint-ignore }} {{model.name}} {{/each}}
    From f5b8ecf269ea59c5a2bca5f430ac48b9133d2326 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 12:15:07 -0400 Subject: [PATCH 082/128] Glint tweaks --- web/app/components/header/search.hbs | 12 ++++++++++-- web/app/components/header/search.ts | 7 ++++++- web/types/glint/index.d.ts | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 6f14871de..c3fa13131 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -1,7 +1,8 @@ -{{! @glint-nocheck: not typesafe yet }} {{on-document "keydown" this.onDocumentKeydown}} +
    + {{! @glint-ignore - not yet registered}} <:anchor as |dd|> + {{! @glint-ignore - not yet registered}} + {{! @glint-ignore - not yet registered}} View all results for "{{this.query}}" @@ -78,12 +81,14 @@ @query={{hash product=(array dd.attrs.product) page=1}} class="search-popover-header-link border-t border-t-color-border-primary" > - {{log dd.attrs.product}} + {{! @glint-ignore - not yet registered}} View all + {{! @glint-ignore - not yet registered}} @@ -99,6 +104,7 @@ @model={{dd.value}} class="flex items-center space-x-3 py-2 px-3 h-20" > + {{! @glint-ignore - not yet registered}} + {{! @glint-ignore - not yet registered}} {{#if dd.attrs._snippetResult.content.value}} + {{! @glint-ignore - not yet registered}} { @service("config") declare configSvc: ConfigService; @service declare algolia: AlgoliaService; @service declare router: RouterService; diff --git a/web/types/glint/index.d.ts b/web/types/glint/index.d.ts index 17b1e3062..9d4406ccf 100644 --- a/web/types/glint/index.d.ts +++ b/web/types/glint/index.d.ts @@ -3,5 +3,12 @@ import "@glint/environment-ember-loose"; declare module "@glint/environment-ember-loose/registry" { export default interface Registry { "did-insert": typeof import("@gavant/glint-template-types/types/ember-render-modifiers/did-insert").default; + "on-document": typeof import("@gavant/glint-template-types/types/ember-on-helper/on-document").default; + perform: typeof import("@gavant/glint-template-types/types/ember-concurrency/perform").default; + or: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/or").default; + eq: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/eq").default; + and: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/and").default; + not: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/not").default; + "is-empty": typeof import("@gavant/glint-template-types/types/ember-truth-helpers/is-empty").default; } } From 671d1ecee09d1609e35ab545b78f200a3879ca15 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 21:07:40 -0400 Subject: [PATCH 083/128] Improve saving state --- web/app/components/document/sidebar.hbs | 12 ++++--- web/app/components/document/sidebar.js | 10 ++++++ .../components/inputs/badge-dropdown-list.hbs | 17 ++++++---- .../components/inputs/badge-dropdown-list.ts | 6 ++++ web/app/components/inputs/people-select.ts | 25 ++++++++------- web/app/components/inputs/product-select.hbs | 32 +++++++++++-------- .../components/inputs/product-select/item.hbs | 19 +++++++++++ .../components/inputs/product-select/item.ts | 17 ++++++++++ .../x/dropdown-list/checkable-item.hbs | 2 +- web/app/components/x/dropdown-list/index.ts | 7 ++-- web/app/styles/app.scss | 1 + .../components/x/dropdown/list-item.scss | 10 +++--- 12 files changed, 114 insertions(+), 44 deletions(-) create mode 100644 web/app/components/inputs/product-select/item.hbs create mode 100644 web/app/components/inputs/product-select/item.ts diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 9540df0d6..169cc06a6 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -12,9 +12,13 @@ {{on "scroll" this.onScroll}} {{did-insert this.registerBody}} > - {{#let (get-product-id @document.product) as |productIcon|}} + {{#let (get-product-id this.selectedProductArea) as |productIcon|}} {{#if productIcon}} -
    +
    {{/if}} @@ -132,7 +136,7 @@ {{#if this.isDraft}}
    {{/if}} -{{/in-element}} \ No newline at end of file +{{/in-element}} diff --git a/web/app/components/document/sidebar.js b/web/app/components/document/sidebar.js index 47a28a509..ed3a0c70d 100644 --- a/web/app/components/document/sidebar.js +++ b/web/app/components/document/sidebar.js @@ -49,6 +49,12 @@ export default class DocumentSidebar extends Component { @tracked userHasScrolled = false; @tracked body = null; + @tracked _newProduct = null; + + get selectedProductArea() { + return this._newProduct || this.args.document.product; + } + get customEditableFields() { let customEditableFields = this.args.document.customEditableFields || {}; for (const field in customEditableFields) { @@ -178,6 +184,7 @@ export default class DocumentSidebar extends Component { } @action updateProduct(product) { + this._newProduct = product; this.product = product; this.save.perform("product", this.product); } @@ -221,6 +228,9 @@ export default class DocumentSidebar extends Component { } catch (err) { // revert field value on failure this[field] = oldVal; + if (field === "product") { + this._newProduct = null; + } } } } diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index 26b6087b3..c6ff43cc9 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck - not typesafe yet }} <:item as |dd|> - - - + {{#if (has-block "item")}} + {{yield dd to="item"}} + {{else}} + + + + {{/if}} diff --git a/web/app/components/inputs/badge-dropdown-list.ts b/web/app/components/inputs/badge-dropdown-list.ts index 12f1bfded..2e6828daa 100644 --- a/web/app/components/inputs/badge-dropdown-list.ts +++ b/web/app/components/inputs/badge-dropdown-list.ts @@ -11,3 +11,9 @@ interface InputsBadgeDropdownListComponentSignature { } export default class InputsBadgeDropdownListComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::BadgeDropdownList": typeof InputsBadgeDropdownListComponent; + } +} diff --git a/web/app/components/inputs/people-select.ts b/web/app/components/inputs/people-select.ts index 27a3f8505..436e5c912 100644 --- a/web/app/components/inputs/people-select.ts +++ b/web/app/components/inputs/people-select.ts @@ -17,6 +17,7 @@ interface PeopleSelectComponentSignature { selected: HermesUser[]; onBlur?: () => void; onChange: (people: GoogleUser[]) => void; + onNewProductClick?: () => void; }; } @@ -76,17 +77,19 @@ export default class PeopleSelectComponent extends Component { - return { - email: p.emailAddresses[0]?.value, - imgURL: p.photos?.[0]?.url, - }; - }).filter((person: HermesUser) => { - // filter out any people already selected - return !this.args.selected.find( - (selectedPerson) => selectedPerson.email === person.email - ); - }); + this.people = peopleJson + .map((p: GoogleUser) => { + return { + email: p.emailAddresses[0]?.value, + imgURL: p.photos?.[0]?.url, + }; + }) + .filter((person: HermesUser) => { + // filter out any people already selected + return !this.args.selected.find( + (selectedPerson) => selectedPerson.email === person.email + ); + }); } else { this.people = []; } diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index b5db0b59d..dc7d4b470 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,4 +1,5 @@ -{{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} +{{! @glint-nocheck - not typesafe yet }} +{{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} {{#if @formatIsBadge}} @@ -11,7 +12,16 @@ @isSaving={{@isSaving}} class="w-80" ...attributes - /> + > + <:item as |dd|> + + + + + {{else}} <:item as |dd|> - - - {{dd.value}} - - {{dd.attrs.abbreviation}} - - {{#if dd.selected}} - - {{/if}} + + diff --git a/web/app/components/inputs/product-select/item.hbs b/web/app/components/inputs/product-select/item.hbs new file mode 100644 index 000000000..2321a738c --- /dev/null +++ b/web/app/components/inputs/product-select/item.hbs @@ -0,0 +1,19 @@ +
    + {{! @glint-ignore - not yet registered }} + + + {{@product}} + + {{#if @abbreviation}} + + {{@abbreviation}} + + {{/if}} + {{#if @selected}} + {{! @glint-ignore - not yet registered }} + + {{/if}} +
    diff --git a/web/app/components/inputs/product-select/item.ts b/web/app/components/inputs/product-select/item.ts new file mode 100644 index 000000000..e64179cc9 --- /dev/null +++ b/web/app/components/inputs/product-select/item.ts @@ -0,0 +1,17 @@ +import Component from "@glimmer/component"; + +interface InputsProductSelectItemComponentSignature { + Args: { + product: string; + selected: boolean; + abbreviation?: boolean; + }; +} + +export default class InputsProductSelectItemComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::ProductSelect::Item": typeof InputsProductSelectItemComponent; + } +} diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index 8717250cd..bb4914b13 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,7 +1,7 @@
    {{@value}} diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index b9f9bfaea..abb748c0a 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -7,7 +7,8 @@ import { tracked } from "@glimmer/tracking"; import { restartableTask } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; -interface XDropdownListComponentSignature { +interface XDropdownListComponentSignature { + Element: HTMLDivElement; Args: { selected: any; items?: any; @@ -23,9 +24,7 @@ export enum FocusDirection { Last = "last", } -export default class XDropdownListComponent extends Component< - XDropdownListComponentSignature -> { +export default class XDropdownListComponent extends Component { @service("fetch") declare fetchSvc: FetchService; @tracked private _scrollContainer: HTMLElement | null = null; diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 5340903e3..3792d63ad 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -9,6 +9,7 @@ @use "components/x-hds-tab"; @use "components/x/dropdown/list"; @use "components/x/dropdown/list-item"; +@use "components/x/dropdown/toggle-select"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index f76264230..210ca5163 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -13,13 +13,15 @@ } } + &:not(.is-aria-selected) { + .check { + @apply text-color-foreground-action; + } + } + .flight-icon { @apply shrink-0; - &.check { - @apply mr-2.5 text-color-foreground-action; - } - &.sort-icon { @apply ml-3 mr-4; } From 219fdfed44662ff140e3ccd5a58ba7912afd439c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 21:16:25 -0400 Subject: [PATCH 084/128] Cleanup --- web/app/components/header/toolbar.hbs | 1 - web/app/components/inputs/people-select.ts | 25 ++++++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/web/app/components/header/toolbar.hbs b/web/app/components/header/toolbar.hbs index 7949942ae..ebe9b6cc8 100644 --- a/web/app/components/header/toolbar.hbs +++ b/web/app/components/header/toolbar.hbs @@ -14,7 +14,6 @@ @label="Type" @facets={{@facets.docType}} @disabled={{not @facets.docType}} - class="" /> void; onChange: (people: GoogleUser[]) => void; - onNewProductClick?: () => void; }; } @@ -77,19 +76,17 @@ export default class PeopleSelectComponent extends Component { - return { - email: p.emailAddresses[0]?.value, - imgURL: p.photos?.[0]?.url, - }; - }) - .filter((person: HermesUser) => { - // filter out any people already selected - return !this.args.selected.find( - (selectedPerson) => selectedPerson.email === person.email - ); - }); + this.people = peopleJson.map((p: GoogleUser) => { + return { + email: p.emailAddresses[0]?.value, + imgURL: p.photos?.[0]?.url, + }; + }).filter((person: HermesUser) => { + // filter out any people already selected + return !this.args.selected.find( + (selectedPerson) => selectedPerson.email === person.email + ); + }); } else { this.people = []; } From 77986b6352920baf1029f9d2591e39d35d9d8090 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 16 May 2023 21:38:08 -0400 Subject: [PATCH 085/128] Glint-related updates --- web/app/components/document/sidebar.js | 4 +--- .../components/inputs/badge-dropdown-list.hbs | 6 +++++- .../components/inputs/badge-dropdown-list.ts | 9 +++++++- web/app/components/inputs/product-select.hbs | 10 +++++++-- web/app/components/inputs/product-select.ts | 10 +++++++++ web/app/components/new/doc-form.hbs | 21 ++++++++++++++++--- web/app/components/new/doc-form.ts | 11 +++++++++- .../x/dropdown-list/checkable-item.ts | 6 ++++++ web/app/components/x/dropdown-list/index.ts | 18 ++++++++++++++-- 9 files changed, 82 insertions(+), 13 deletions(-) diff --git a/web/app/components/document/sidebar.js b/web/app/components/document/sidebar.js index ed3a0c70d..8fda2bf92 100644 --- a/web/app/components/document/sidebar.js +++ b/web/app/components/document/sidebar.js @@ -228,9 +228,7 @@ export default class DocumentSidebar extends Component { } catch (err) { // revert field value on failure this[field] = oldVal; - if (field === "product") { - this._newProduct = null; - } + this._newProduct = null; } } } diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index c6ff43cc9..22306c2cd 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck - not typesafe yet }} {{#if @isSaving}}
    + {{! @glint-ignore - not registered yet }}
    {{/if}} + {{! @glint-ignore - not registered yet }} + {{! @glint-ignore - not registered yet }} void; + onItemClick: (e: Event) => void; + placement?: Placement; + }; + Blocks: { + default: []; + item: [dd: any]; }; } diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index dc7d4b470..b5b5db2a9 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck - not typesafe yet }} {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} {{#if this.products}} @@ -37,8 +36,12 @@ + {{! @glint-ignore - not registered yet }} - {{or @selected "--"}} + + {{! @glint-ignore - not registered yet }} + {{or @selected "--"}} + {{#if this.selectedProductAbbreviation}} {{/if}} + {{! @glint-ignore - not registered yet }} {{/if}} {{else if this.fetchProducts.isRunning}} + {{! @glint-ignore - not registered yet }} {{else}}
    {{/if}} diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 379df0d32..456a6bba5 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -1,6 +1,7 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; @@ -8,11 +9,14 @@ import FetchService from "hermes/services/fetch"; import { BadgeSize } from "hermes/types/hds-badge"; interface InputsProductSelectSignatureSignature { + Element: HTMLDivElement; Args: { selected?: any; onChange: (value: string, abbreviation: string) => void; badgeSize?: BadgeSize; formatIsBadge?: boolean; + placement?: Placement; + isSaving?: boolean; }; } @@ -56,3 +60,9 @@ export default class InputsProductSelectSignature extends Component + {{! @glint-ignore: not yet registered }}
    Creating @@ -14,6 +14,7 @@
    @@ -23,6 +24,7 @@ >Create your {{@docType}}
    + {{! @glint-ignore: not yet registered }}
    + {{! @glint-ignore: not yet registered }} Product/Area   + {{! @glint-ignore - not registered yet }} @@ -114,6 +118,7 @@ --}} + {{! @glint-ignore - not registered yet }} {{yield (hash @@ -125,13 +130,16 @@ ) }} + {{! @glint-ignore - not registered yet }} - + + {{! @glint-ignore - not registered yet }} + Contributors {{#if this.formErrors.contributors}} @@ -148,16 +156,23 @@
    -

    Preview

    +

    + {{! @glint-ignore - not registered yet }} + + Preview +

    + {{! @glint-ignore - not registered yet }} + {{! @glint-ignore - not registered yet }} diff --git a/web/app/components/new/doc-form.ts b/web/app/components/new/doc-form.ts index 5813e594a..034689b0b 100644 --- a/web/app/components/new/doc-form.ts +++ b/web/app/components/new/doc-form.ts @@ -172,7 +172,10 @@ export default class NewDocFormComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "X::DropdownList::CheckableItem": typeof XDropdownListCheckableItemComponent; + } +} diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index abb748c0a..565d14e80 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -2,6 +2,7 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; +import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { restartableTask } from "ember-concurrency"; @@ -10,10 +11,17 @@ import FetchService from "hermes/services/fetch"; interface XDropdownListComponentSignature { Element: HTMLDivElement; Args: { - selected: any; items?: any; - onChange: (value: any) => void; listIsOrdered?: boolean; + selected: any; + placement?: Placement; + isSaving?: boolean; + onItemClick: (value: any) => void; + }; + Blocks: { + default: []; + anchor: [dd: any]; + item: [dd: any]; }; } @@ -289,3 +297,9 @@ export default class XDropdownListComponent extends Component Date: Wed, 17 May 2023 15:32:25 -0400 Subject: [PATCH 086/128] Glint types, todo test --- web/app/components/document/sidebar.hbs | 5 +- .../components/inputs/badge-dropdown-list.hbs | 6 +- web/app/components/inputs/product-select.hbs | 150 +++++++++--------- .../components/document/sidebar-test.ts | 47 ++++-- .../document/sidebar/header-test.ts | 6 +- web/types/glint/index.d.ts | 13 ++ 6 files changed, 133 insertions(+), 94 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 169cc06a6..17fbed164 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -40,7 +40,10 @@ (is-empty @document.docType) }}{{@document.docType}}{{/unless}} • - {{@document.docNumber}} + + {{@document.docNumber}} + + {{/if}} {{#if this.editingIsDisabled}}

    {{/if}} - + {{! @glint-ignore - not registered yet }} - <:item as |dd|> - - - - - - {{else}} - - <:anchor as |dd|> - - {{! @glint-ignore - not registered yet }} - - +
    + {{#if this.products}} + {{#if @formatIsBadge}} + + <:item as |dd|> + + + + + + {{else}} + + <:anchor as |dd|> + {{! @glint-ignore - not registered yet }} - {{or @selected "--"}} - - {{#if this.selectedProductAbbreviation}} - - {{this.selectedProductAbbreviation}} + + + {{or @selected "--"}} - {{/if}} - {{! @glint-ignore - not registered yet }} - - - - <:item as |dd|> - - - - - - {{/if}} -{{else if this.fetchProducts.isRunning}} - {{! @glint-ignore - not registered yet }} - -{{else}} -
    + {{this.selectedProductAbbreviation}} + + {{/if}} + {{! @glint-ignore - not registered yet }} + + + + <:item as |dd|> + + + + + + {{/if}} + {{else if this.fetchProducts.isRunning}} {{! @glint-ignore - not registered yet }} - {{did-insert (perform this.fetchProducts)}} - >
    -{{/if}} + + {{else}} +
    + {{/if}} +
    diff --git a/web/tests/integration/components/document/sidebar-test.ts b/web/tests/integration/components/document/sidebar-test.ts index 5cd0391f7..dcbce13e5 100644 --- a/web/tests/integration/components/document/sidebar-test.ts +++ b/web/tests/integration/components/document/sidebar-test.ts @@ -1,14 +1,23 @@ -import { module, test } from "qunit"; +import { module, test, todo } from "qunit"; import { setupRenderingTest } from "ember-qunit"; -import { findAll, render, select } from "@ember/test-helpers"; +import { click, findAll, render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; +import { AuthenticatedUser } from "hermes/services/authenticated-user"; +import { HermesDocument } from "hermes/types/document"; module("Integration | Component | document/sidebar", function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); - test("you can change a draft's product area", async function (this: MirageTestContext, assert) { + interface DocumentSidebarTestContext extends MirageTestContext { + profile: AuthenticatedUser; + document: HermesDocument; + deleteDraft: () => {}; + docType: string; + } + + todo("you can change a draft's product area", async function (this: DocumentSidebarTestContext, assert) { this.server.createList("product", 3); const docID = "test-doc-0"; @@ -24,6 +33,7 @@ module("Integration | Component | document/sidebar", function (hooks) { this.set("noop", () => {}); await render(hbs` + {{! @glint-nocheck - not yet typed}} `); - assert - .dom("[data-test-sidebar-product-select]") - .exists("drafts show a product select element") - .hasValue("Test Product 1", "The document product is selected"); + const docNumberSelector = "[data-test-sidebar-doc-number]"; + const productSelectSelector = "[data-test-sidebar-product-select]"; + const productSelectTriggerSelector = "[data-test-badge-dropdown-trigger]"; + const productSelectDropdownItemSelector = + "[data-test-product-select-badge-dropdown-item]"; assert - .dom("[data-test-sidebar-doc-number]") + .dom(docNumberSelector) .hasText("TST-001", "The document number is correct"); - const options = findAll("[data-test-sidebar-product-select] option"); + assert + .dom(productSelectSelector) + .exists("drafts show a product select element") + .hasText("Test Product 1", "The document product is selected"); + + await click(productSelectTriggerSelector); + + const options = findAll(productSelectDropdownItemSelector); const expectedProducts = [ - "", // The first option is blank "Test Product 0", "Test Product 1", "Test Product 2", @@ -52,19 +69,19 @@ module("Integration | Component | document/sidebar", function (hooks) { options.forEach((option: Element, index: number) => { assert.equal( - option.textContent, + option.textContent?.trim(), expectedProducts[index], "the product is correct" ); }); - await select("[data-test-sidebar-product-select]", "Test Product 0"); + // FIXME: Test hangs here, maybe due to the refreshRoute() method + // await click(productSelectDropdownItemSelector); /** * Mirage properties aren't reactive like Ember's, so we * need to manually update the document. */ - const refreshMirageDocument = () => { this.set( "document", @@ -75,7 +92,7 @@ module("Integration | Component | document/sidebar", function (hooks) { refreshMirageDocument(); assert - .dom("[data-test-sidebar-doc-number]") + .dom(docNumberSelector) .hasText("TST-000", "The document is patched with the correct docNumber"); this.server.schema.document @@ -85,7 +102,7 @@ module("Integration | Component | document/sidebar", function (hooks) { refreshMirageDocument(); assert - .dom("[data-test-sidebar-product-select]") + .dom(productSelectSelector) .doesNotExist("The product select is not shown for published documents"); }); }); diff --git a/web/tests/integration/components/document/sidebar/header-test.ts b/web/tests/integration/components/document/sidebar/header-test.ts index 5b44d0adf..843d2842e 100644 --- a/web/tests/integration/components/document/sidebar/header-test.ts +++ b/web/tests/integration/components/document/sidebar/header-test.ts @@ -28,7 +28,11 @@ module("Integration | Component | document/sidebar/header", function (hooks) { }); test("it renders as expected", async function (this: DocumentSidebarHeaderTestContext, assert) { - this.server.create("document", { objectID: "400" }); + this.server.create("document", { + objectID: "400", + status: "in-review", + docNumber: "001", + }); this.set("document", this.server.schema.document.first().attrs); await render(hbs` diff --git a/web/types/glint/index.d.ts b/web/types/glint/index.d.ts index bb286ee63..9d4406ccf 100644 --- a/web/types/glint/index.d.ts +++ b/web/types/glint/index.d.ts @@ -1 +1,14 @@ import "@glint/environment-ember-loose"; + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "did-insert": typeof import("@gavant/glint-template-types/types/ember-render-modifiers/did-insert").default; + "on-document": typeof import("@gavant/glint-template-types/types/ember-on-helper/on-document").default; + perform: typeof import("@gavant/glint-template-types/types/ember-concurrency/perform").default; + or: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/or").default; + eq: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/eq").default; + and: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/and").default; + not: typeof import("@gavant/glint-template-types/types/ember-truth-helpers/not").default; + "is-empty": typeof import("@gavant/glint-template-types/types/ember-truth-helpers/is-empty").default; + } +} From 4f71d00079d4add72f77ff244c7bdbd432a5db72 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 17 May 2023 16:38:59 -0400 Subject: [PATCH 087/128] Start of badgeDropdownList tests --- .../components/inputs/badge-dropdown-list.hbs | 12 +- .../components/inputs/badge-dropdown-list.ts | 2 +- .../x/dropdown-list/checkable-item.hbs | 1 + web/app/components/x/dropdown-list/item.ts | 1 + .../inputs/badge-dropdown-list-test.ts | 112 ++++++++++++++++++ 5 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 web/tests/integration/components/inputs/badge-dropdown-list-test.ts diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index c3fabfef5..aae794f23 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -10,7 +10,10 @@ {{#if @isSaving}}
    {{! @glint-ignore - not registered yet }} - +
    {{/if}} {{! @glint-ignore - not registered yet }} {{! @glint-ignore - not registered yet }} @@ -36,7 +44,7 @@ {{#if (has-block "item")}} {{yield dd to="item"}} {{else}} - + void; + onItemClick: ((e: Event) => void) | ((e: string) => void); placement?: Placement; }; Blocks: { diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index bb4914b13..34df78561 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,5 +1,6 @@ diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index 9f7e03fcf..033a8d079 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -89,6 +89,7 @@ export default class XDropdownListItemComponent extends Component handling. */ next(() => { + console.log('uh'); this.args.hideDropdown(); }); } diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts new file mode 100644 index 000000000..eb983a29a --- /dev/null +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -0,0 +1,112 @@ +import { module, test, todo } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { + click, + fillIn, + findAll, + render, + waitFor, + waitUntil, +} from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; +import { HermesUser } from "hermes/types/document"; +import FetchService from "hermes/services/fetch"; +import { Placement } from "@floating-ui/dom"; +import { timeout } from "ember-concurrency"; + +interface BadgeDropdownListTestContext extends MirageTestContext { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: ((e: Event) => void) | ((selected: string) => void); + placement?: Placement; +} + +module( + "Integration | Component | inputs/badge-dropdown-list", + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + todo( + "it functions as expected (default checkable item)", + async function (this: BadgeDropdownListTestContext, assert) { + this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; + this.selected = Object.keys(this.items)[1]; + this.isSaving = false; + + this.onItemClick = (selected: string) => { + this.set("isSaving", true); + this.set("selected", selected); + + setTimeout(() => { + this.set("isSaving", false); + }, 100); + }; + + await render(hbs` + {{! @glint-ignore: not typed yet }} + + `); + + const iconSelector = "[data-test-badge-dropdown-list-icon]"; + const triggerSelector = "[data-test-badge-dropdown-trigger]"; + const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]"; + const savingIconSelector = + "[data-test-badge-dropdown-list-saving-icon]"; + const itemSelector = "[data-test-badge-dropdown-list-default-action]"; + + assert.dom(iconSelector).hasAttribute("data-test-icon", "folder"); + assert.dom(triggerSelector).hasText("Labs"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + + assert.dom(savingIconSelector).doesNotExist(); + + await click(triggerSelector); + + let listItemsText = findAll(itemSelector).map((el) => el.textContent); + assert.deepEqual(listItemsText, ["foo", "bar", "waypoint"]); + + assert + .dom( + `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` + ) + .hasAttribute("data-test-is-checked", "true"); + + // let clickPromise = click(itemSelector); + + // await waitFor(savingIconSelector); + + // Note: This is where the TODO test fails (as we expect) + assert.dom(savingIconSelector).exists(); + + /** + * FIXME: Something is causing the test to hang here + * Likely to do with the `next` runloop. + */ + + // await clickPromise; + + // await waitUntil(() => !this.isSaving); + + assert.dom(savingIconSelector).doesNotExist(); + + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "up"); + assert.dom(triggerSelector).hasText("Waypoint"); + assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); + } + ); + } +); From 0699c821de86f658c7c32b2e0e8fe41ba7ce5224 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 12:02:51 -0400 Subject: [PATCH 088/128] Move tests to acceptance --- web/mirage/config.ts | 1 - web/mirage/factories/document.ts | 6 +- .../acceptance/authenticated/document-test.ts | 84 +++++++++++++- .../components/document/sidebar-test.ts | 108 ------------------ 4 files changed, 87 insertions(+), 112 deletions(-) delete mode 100644 web/tests/integration/components/document/sidebar-test.ts diff --git a/web/mirage/config.ts b/web/mirage/config.ts index 26c22b5f5..a98789634 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -317,7 +317,6 @@ export default function (mirageConfig) { } document.update(attrs); - return new Response(200, {}, document.attrs); } }); diff --git a/web/mirage/factories/document.ts b/web/mirage/factories/document.ts index 9014233dc..13b905c1c 100644 --- a/web/mirage/factories/document.ts +++ b/web/mirage/factories/document.ts @@ -3,9 +3,11 @@ import { Factory } from "miragejs"; export function getTestProductAbbreviation(product: string) { switch (product) { case "Test Product 0": - return "TST-000"; + return "TST-0"; case "Test Product 1": - return "TST-001"; + return "TST-1"; + case "Test Product 2": + return "TST-2"; } } diff --git a/web/tests/acceptance/authenticated/document-test.ts b/web/tests/acceptance/authenticated/document-test.ts index d5a87e490..704242ca5 100644 --- a/web/tests/acceptance/authenticated/document-test.ts +++ b/web/tests/acceptance/authenticated/document-test.ts @@ -1,4 +1,4 @@ -import { visit } from "@ember/test-helpers"; +import { click, findAll, visit } from "@ember/test-helpers"; import { setupApplicationTest } from "ember-qunit"; import { module, test } from "qunit"; import { authenticateSession } from "ember-simple-auth/test-support"; @@ -30,4 +30,86 @@ module("Acceptance | authenticated/document", function (hooks) { await visit("/document/1?draft=true"); assert.equal(getPageTitle(), "Test Document | Hermes"); }); + + test("you can change a draft's product area", async function (this: AuthenticatedDocumentRouteTestContext, assert) { + const docID = "test-doc-0"; + + this.server.createList("product", 3); + + const initialProduct = this.server.schema.products.find(2).attrs; + + const initialProductName = initialProduct.name; + const initialProductAbbreviation = initialProduct.abbreviation; + + const targetProductAbbreviation = + this.server.schema.products.find(1).attrs.abbreviation; + + this.server.create("document", { + objectID: docID, + isDraft: true, + product: initialProductName, + }); + + await visit(`/document/${docID}?draft=true`); + + const docNumberSelector = "[data-test-sidebar-doc-number]"; + const productSelectSelector = "[data-test-sidebar-product-select]"; + const productSelectTriggerSelector = "[data-test-badge-dropdown-trigger]"; + const productSelectDropdownItemSelector = + "[data-test-product-select-badge-dropdown-item]"; + + assert + .dom(docNumberSelector) + .hasText(initialProductAbbreviation, "The document number is correct"); + + assert + .dom(productSelectSelector) + .exists("drafts show a product select element") + .hasText(initialProductName, "The document product is selected"); + + await click(productSelectTriggerSelector); + const options = findAll(productSelectDropdownItemSelector); + + const expectedProducts = [ + "Test Product 0", + "Test Product 1", + "Test Product 2", + ]; + + options.forEach((option: Element, index: number) => { + assert.equal( + option.textContent?.trim(), + expectedProducts[index], + "the product is correct" + ); + }); + + await click(productSelectDropdownItemSelector); + + assert + .dom(docNumberSelector) + .hasText( + targetProductAbbreviation, + "The document is patched with the correct docNumber" + ); + + this.server.schema.document + .findBy({ objectID: docID }) + .update("isDraft", false); + }); + + test("a published doc's productArea can't be changed ", async function (this: AuthenticatedDocumentRouteTestContext, assert) { + this.server.create("document", { + objectID: 1, + title: "Test Document", + isDraft: false, + product: "Test Product 0", + }); + + await visit("/document/1"); + + assert + .dom("[data-test-sidebar-product-select]") + .doesNotExist("published docs don't show a product select element"); + }); }); diff --git a/web/tests/integration/components/document/sidebar-test.ts b/web/tests/integration/components/document/sidebar-test.ts deleted file mode 100644 index dcbce13e5..000000000 --- a/web/tests/integration/components/document/sidebar-test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { module, test, todo } from "qunit"; -import { setupRenderingTest } from "ember-qunit"; -import { click, findAll, render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; -import { AuthenticatedUser } from "hermes/services/authenticated-user"; -import { HermesDocument } from "hermes/types/document"; - -module("Integration | Component | document/sidebar", function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - interface DocumentSidebarTestContext extends MirageTestContext { - profile: AuthenticatedUser; - document: HermesDocument; - deleteDraft: () => {}; - docType: string; - } - - todo("you can change a draft's product area", async function (this: DocumentSidebarTestContext, assert) { - this.server.createList("product", 3); - - const docID = "test-doc-0"; - const profile = this.server.create("me"); - const document = this.server.create("document", { - objectID: docID, - isDraft: true, - product: "Test Product 1", - }); - - this.set("profile", profile); - this.set("document", document); - this.set("noop", () => {}); - - await render(hbs` - {{! @glint-nocheck - not yet typed}} - - `); - - const docNumberSelector = "[data-test-sidebar-doc-number]"; - const productSelectSelector = "[data-test-sidebar-product-select]"; - const productSelectTriggerSelector = "[data-test-badge-dropdown-trigger]"; - const productSelectDropdownItemSelector = - "[data-test-product-select-badge-dropdown-item]"; - - assert - .dom(docNumberSelector) - .hasText("TST-001", "The document number is correct"); - - assert - .dom(productSelectSelector) - .exists("drafts show a product select element") - .hasText("Test Product 1", "The document product is selected"); - - await click(productSelectTriggerSelector); - - const options = findAll(productSelectDropdownItemSelector); - - const expectedProducts = [ - "Test Product 0", - "Test Product 1", - "Test Product 2", - ]; - - options.forEach((option: Element, index: number) => { - assert.equal( - option.textContent?.trim(), - expectedProducts[index], - "the product is correct" - ); - }); - - // FIXME: Test hangs here, maybe due to the refreshRoute() method - // await click(productSelectDropdownItemSelector); - - /** - * Mirage properties aren't reactive like Ember's, so we - * need to manually update the document. - */ - const refreshMirageDocument = () => { - this.set( - "document", - this.server.schema.document.findBy({ objectID: docID }) - ); - }; - - refreshMirageDocument(); - - assert - .dom(docNumberSelector) - .hasText("TST-000", "The document is patched with the correct docNumber"); - - this.server.schema.document - .findBy({ objectID: docID }) - .update("isDraft", false); - - refreshMirageDocument(); - - assert - .dom(productSelectSelector) - .doesNotExist("The product select is not shown for published documents"); - }); -}); From 26853082b6d442f78e67c1590efcb90ea2bb8ef0 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 12:27:28 -0400 Subject: [PATCH 089/128] Simplify glint-nochecks --- web/app/components/inputs/badge-dropdown-list.hbs | 6 +----- web/app/components/inputs/product-select.hbs | 5 +---- web/app/components/inputs/product-select/item.hbs | 3 +-- web/app/components/new/doc-form.hbs | 13 +------------ 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index aae794f23..813ebe08b 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck - not typesafe yet }} {{#if @isSaving}}
    - {{! @glint-ignore - not registered yet }} - {{! @glint-ignore - not registered yet }} - {{! @glint-ignore - not registered yet }} {{#if this.products}} @@ -36,7 +37,6 @@ - {{! @glint-ignore - not registered yet }} {{or @selected "--"}} @@ -48,7 +48,6 @@ {{this.selectedProductAbbreviation}} {{/if}} - {{! @glint-ignore - not registered yet }} {{/if}} {{else if this.fetchProducts.isRunning}} - {{! @glint-ignore - not registered yet }} {{else}}
    {{/if}} diff --git a/web/app/components/inputs/product-select/item.hbs b/web/app/components/inputs/product-select/item.hbs index 2321a738c..dd8d884b7 100644 --- a/web/app/components/inputs/product-select/item.hbs +++ b/web/app/components/inputs/product-select/item.hbs @@ -1,5 +1,5 @@ +{{! @glint-nocheck - not typesafe yet }}
    - {{! @glint-ignore - not yet registered }} {{@product}} @@ -10,7 +10,6 @@ {{/if}} {{#if @selected}} - {{! @glint-ignore - not yet registered }} - {{! @glint-ignore: not yet registered }}
    Creating @@ -14,7 +14,6 @@
    @@ -24,7 +23,6 @@ >Create your {{@docType}}
    - {{! @glint-ignore: not yet registered }}
    - {{! @glint-ignore: not yet registered }} Product/Area   - {{! @glint-ignore - not registered yet }} @@ -118,7 +114,6 @@ --}} - {{! @glint-ignore - not registered yet }} {{yield (hash @@ -130,7 +125,6 @@ ) }} - {{! @glint-ignore - not registered yet }} - {{! @glint-ignore - not registered yet }} Contributors @@ -157,22 +150,18 @@

    - {{! @glint-ignore - not registered yet }} Preview

    - {{! @glint-ignore - not registered yet }} - {{! @glint-ignore - not registered yet }} From a254919df7c2685015fa78277965d3608c1a4fe2 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 15:01:07 -0400 Subject: [PATCH 090/128] Fix broken tests --- web/app/components/document/sidebar.hbs | 2 +- web/app/components/document/sidebar.js | 10 +- .../components/inputs/badge-dropdown-list.hbs | 4 +- web/app/components/inputs/product-select.ts | 6 +- web/app/components/x/dropdown-list/item.ts | 24 +++- .../inputs/badge-dropdown-list-test.ts | 122 +++++++----------- .../components/x/dropdown-list/index-test.ts | 4 - 7 files changed, 79 insertions(+), 93 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index 17fbed164..c0dbf3dea 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -140,7 +140,7 @@
    diff --git a/web/app/components/document/sidebar.js b/web/app/components/document/sidebar.js index 8fda2bf92..be9535c92 100644 --- a/web/app/components/document/sidebar.js +++ b/web/app/components/document/sidebar.js @@ -51,6 +51,11 @@ export default class DocumentSidebar extends Component { @tracked _newProduct = null; + /** + * The currently selected product. If the user is saving a new product, + * this will be the new product. Otherwise, it will be the product that the + * saved document is associated with. + */ get selectedProductArea() { return this._newProduct || this.args.document.product; } @@ -183,10 +188,11 @@ export default class DocumentSidebar extends Component { getOwner(this).lookup(`route:${this.router.currentRouteName}`).refresh(); } - @action updateProduct(product) { + @task({ restartable: true }) + *updateProduct(product) { this._newProduct = product; this.product = product; - this.save.perform("product", this.product); + yield this.save.perform("product", this.product); } @action maybeShowFlashError(error, title) { diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index 813ebe08b..9d06689f1 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -23,15 +23,15 @@
    diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 456a6bba5..95c8a2904 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -8,7 +8,7 @@ import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; import { BadgeSize } from "hermes/types/hds-badge"; -interface InputsProductSelectSignatureSignature { +interface InputsProductSelectSignature { Element: HTMLDivElement; Args: { selected?: any; @@ -27,7 +27,7 @@ type ProductAreas = { }; }; -export default class InputsProductSelectSignature extends Component { +export default class InputsProductSelectComponent extends Component { @service("fetch") declare fetchSvc: FetchService; @tracked selected = this.args.selected; @@ -63,6 +63,6 @@ export default class InputsProductSelectSignature extends Component handling. + * In production, closes the dropdown on the next run loop + * so that we don't interfere with Ember's handling. + * This creates issues in the testing environment, so we + * use `schedule` as an approximation. + * + * TODO: Improve this. */ - next(() => { - console.log('uh'); - this.args.hideDropdown(); - }); + if (Ember.testing) { + schedule("afterRender", () => { + this.args.hideDropdown(); + }); + } else { + next(() => { + this.args.hideDropdown(); + }); + } } /** diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts index eb983a29a..f294b8068 100644 --- a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -1,20 +1,16 @@ -import { module, test, todo } from "qunit"; +import { module, test } from "qunit"; import { setupRenderingTest } from "ember-qunit"; import { hbs } from "ember-cli-htmlbars"; import { click, - fillIn, findAll, render, - waitFor, + teardownContext, waitUntil, } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; -import { HermesUser } from "hermes/types/document"; -import FetchService from "hermes/services/fetch"; import { Placement } from "@floating-ui/dom"; -import { timeout } from "ember-concurrency"; interface BadgeDropdownListTestContext extends MirageTestContext { items: any; @@ -31,82 +27,60 @@ module( setupRenderingTest(hooks); setupMirage(hooks); - todo( - "it functions as expected (default checkable item)", - async function (this: BadgeDropdownListTestContext, assert) { - this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; - this.selected = Object.keys(this.items)[1]; - this.isSaving = false; + test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) { + this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; + this.selected = Object.keys(this.items)[1]; + this.onItemClick = (selected: string) => { + this.set("selected", selected); + }; - this.onItemClick = (selected: string) => { - this.set("isSaving", true); - this.set("selected", selected); - - setTimeout(() => { - this.set("isSaving", false); - }, 100); - }; - - await render(hbs` + await render(hbs` {{! @glint-ignore: not typed yet }} `); - const iconSelector = "[data-test-badge-dropdown-list-icon]"; - const triggerSelector = "[data-test-badge-dropdown-trigger]"; - const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]"; - const savingIconSelector = - "[data-test-badge-dropdown-list-saving-icon]"; - const itemSelector = "[data-test-badge-dropdown-list-default-action]"; - - assert.dom(iconSelector).hasAttribute("data-test-icon", "folder"); - assert.dom(triggerSelector).hasText("Labs"); - assert - .dom(chevronSelector) - .hasAttribute("data-test-chevron-position", "down"); - - assert.dom(savingIconSelector).doesNotExist(); - - await click(triggerSelector); - - let listItemsText = findAll(itemSelector).map((el) => el.textContent); - assert.deepEqual(listItemsText, ["foo", "bar", "waypoint"]); - - assert - .dom( - `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` - ) - .hasAttribute("data-test-is-checked", "true"); - - // let clickPromise = click(itemSelector); - - // await waitFor(savingIconSelector); - - // Note: This is where the TODO test fails (as we expect) - assert.dom(savingIconSelector).exists(); - - /** - * FIXME: Something is causing the test to hang here - * Likely to do with the `next` runloop. - */ - - // await clickPromise; - - // await waitUntil(() => !this.isSaving); - - assert.dom(savingIconSelector).doesNotExist(); - - assert - .dom(chevronSelector) - .hasAttribute("data-test-chevron-position", "up"); - assert.dom(triggerSelector).hasText("Waypoint"); - assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); - } - ); + const iconSelector = "[data-test-badge-dropdown-list-icon] .flight-icon"; + const triggerSelector = "[data-test-badge-dropdown-trigger]"; + const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]"; + const itemSelector = "[data-test-x-dropdown-list-item]"; + const itemActionSelector = + "[data-test-badge-dropdown-list-default-action]"; + + assert.dom(iconSelector).hasAttribute("data-test-icon", "folder"); + assert.dom(triggerSelector).hasText("Labs"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + + await click(triggerSelector); + + let listItemsText = findAll(itemActionSelector).map((el) => + el.textContent?.trim() + ); + + assert.deepEqual( + listItemsText, + ["Waypoint", "Labs", "Boundary"], + "correct list items are rendered" + ); + + assert + .dom( + `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` + ) + .hasAttribute("data-test-is-checked"); + + await click(itemActionSelector); + + assert.dom(triggerSelector).hasText("Waypoint"); + assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + }); } ); 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 c9687240b..4a126c2e4 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -65,14 +65,10 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "the correct aria-controls attribute is set" ); - await click("[data-test-toggle]"); - assert .dom("[data-test-x-dropdown-list-input]") .doesNotExist("The input is not shown"); - await click("[data-test-toggle]"); - this.set("items", LONG_ITEM_LIST); await click("[data-test-toggle]"); From db5eaa9b5e079d3526ae041aef47f2f159c54a60 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 15:04:35 -0400 Subject: [PATCH 091/128] Remove old code --- web/app/components/inputs/product-select.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select.ts index 95c8a2904..e4b3b21c4 100644 --- a/web/app/components/inputs/product-select.ts +++ b/web/app/components/inputs/product-select.ts @@ -1,4 +1,3 @@ -import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { inject as service } from "@ember/service"; import { Placement } from "@floating-ui/dom"; @@ -12,7 +11,7 @@ interface InputsProductSelectSignature { Element: HTMLDivElement; Args: { selected?: any; - onChange: (value: string, abbreviation: string) => void; + onChange: (value: string) => void; badgeSize?: BadgeSize; formatIsBadge?: boolean; placement?: Placement; @@ -40,12 +39,7 @@ export default class InputsProductSelectComponent extends Component { From bfc74d5c6bf6f43bb522ceb0bd310d1a69b02baa Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 15:34:33 -0400 Subject: [PATCH 092/128] Test fixes and improvements --- web/app/components/inputs/product-select.hbs | 2 +- web/app/components/new/doc-form.ts | 2 +- web/mirage/factories/document.ts | 19 ++++++-- web/mirage/factories/product.ts | 2 +- .../acceptance/authenticated/document-test.ts | 8 ++-- .../acceptance/authenticated/new/doc-test.ts | 47 ++++++++++++++++++- .../components/x/dropdown-list/index-test.ts | 4 ++ 7 files changed, 70 insertions(+), 14 deletions(-) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select.hbs index a35726cc9..bfe63ee33 100644 --- a/web/app/components/inputs/product-select.hbs +++ b/web/app/components/inputs/product-select.hbs @@ -1,6 +1,6 @@ {{! @glint-nocheck - not typesafe yet }} {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} -
    +
    {{#if this.products}} {{#if @formatIsBadge}} `Test Product ${i}`, - abbreviation: (i: number) => `TST-${i}`, + abbreviation: (i: number) => `TP${i}`, }); diff --git a/web/tests/acceptance/authenticated/document-test.ts b/web/tests/acceptance/authenticated/document-test.ts index 704242ca5..901d145f7 100644 --- a/web/tests/acceptance/authenticated/document-test.ts +++ b/web/tests/acceptance/authenticated/document-test.ts @@ -39,7 +39,7 @@ module("Acceptance | authenticated/document", function (hooks) { const initialProduct = this.server.schema.products.find(2).attrs; const initialProductName = initialProduct.name; - const initialProductAbbreviation = initialProduct.abbreviation; + const initialProductAbbreviation = initialProduct.abbreviation + "-001"; const targetProductAbbreviation = this.server.schema.products.find(1).attrs.abbreviation; @@ -53,7 +53,7 @@ module("Acceptance | authenticated/document", function (hooks) { await visit(`/document/${docID}?draft=true`); const docNumberSelector = "[data-test-sidebar-doc-number]"; - const productSelectSelector = "[data-test-sidebar-product-select]"; + const productSelectSelector = "[data-test-product-select]"; const productSelectTriggerSelector = "[data-test-badge-dropdown-trigger]"; const productSelectDropdownItemSelector = "[data-test-product-select-badge-dropdown-item]"; @@ -89,7 +89,7 @@ module("Acceptance | authenticated/document", function (hooks) { assert .dom(docNumberSelector) .hasText( - targetProductAbbreviation, + targetProductAbbreviation + "-001", "The document is patched with the correct docNumber" ); @@ -109,7 +109,7 @@ module("Acceptance | authenticated/document", function (hooks) { await visit("/document/1"); assert - .dom("[data-test-sidebar-product-select]") + .dom("[data-test-product-select]") .doesNotExist("published docs don't show a product select element"); }); }); diff --git a/web/tests/acceptance/authenticated/new/doc-test.ts b/web/tests/acceptance/authenticated/new/doc-test.ts index 0ba462a1f..22f605c5d 100644 --- a/web/tests/acceptance/authenticated/new/doc-test.ts +++ b/web/tests/acceptance/authenticated/new/doc-test.ts @@ -1,4 +1,4 @@ -import { visit } from "@ember/test-helpers"; +import { click, visit } from "@ember/test-helpers"; import { setupApplicationTest } from "ember-qunit"; import { module, test } from "qunit"; import { authenticateSession } from "ember-simple-auth/test-support"; @@ -13,7 +13,6 @@ module("Acceptance | authenticated/new/doc", function (hooks) { hooks.beforeEach(async function (this: AuthenticatedNewDocRouteTestContext) { await authenticateSession({}); - this.server.createList("product", 5); }); test("the page title is correct (RFC)", async function (this: AuthenticatedNewDocRouteTestContext, assert) { @@ -25,4 +24,48 @@ module("Acceptance | authenticated/new/doc", function (hooks) { await visit("/new/doc?docType=PRD"); assert.equal(getPageTitle(), "Create Your PRD | Hermes"); }); + + test("the product/area can be set", async function (this: AuthenticatedNewDocRouteTestContext, assert) { + this.server.createList("product", 4); + + // add a product with an icon + this.server.create("product", { + name: "Terraform", + abbreviation: "TF", + }); + + await visit("/new/doc?docType=RFC"); + + const toggleSelector = "[data-test-x-dropdown-list-toggle-action]"; + const thumbnailBadgeSelector = "[data-test-doc-thumbnail-product-badge]"; + + assert.dom(toggleSelector).exists(); + assert.dom(`${toggleSelector} span`).hasText("--"); + assert + .dom(`${toggleSelector} .flight-icon`) + .hasAttribute("data-test-icon", "folder"); + + assert + .dom(thumbnailBadgeSelector) + .doesNotExist("badge not shown unless a product shortname exists"); + + await click(toggleSelector); + + const listItemSelector = "[data-test-x-dropdown-list-item]"; + const lastItemSelector = `${listItemSelector}:last-child`; + + assert.dom(listItemSelector).exists({ count: 5 }); + assert.dom(lastItemSelector).hasText("Terraform TF"); + assert + .dom(lastItemSelector + " .flight-icon") + .hasAttribute("data-test-icon", "terraform"); + + await click(lastItemSelector + " button"); + + assert.dom(toggleSelector).hasText("Terraform TF"); + assert + .dom(toggleSelector + " .flight-icon") + .hasAttribute("data-test-icon", "terraform"); + assert.dom(thumbnailBadgeSelector).exists(); + }); }); 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 4a126c2e4..c9687240b 100644 --- a/web/tests/integration/components/x/dropdown-list/index-test.ts +++ b/web/tests/integration/components/x/dropdown-list/index-test.ts @@ -65,10 +65,14 @@ module("Integration | Component | x/dropdown-list", function (hooks) { "the correct aria-controls attribute is set" ); + await click("[data-test-toggle]"); + assert .dom("[data-test-x-dropdown-list-input]") .doesNotExist("The input is not shown"); + await click("[data-test-toggle]"); + this.set("items", LONG_ITEM_LIST); await click("[data-test-toggle]"); From 6d0a40170a2ca484409406bf1568dfd41bf851fc Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 15:36:41 -0400 Subject: [PATCH 093/128] File renames --- .../inputs/{product-select.hbs => product-select/index.hbs} | 0 .../inputs/{product-select.ts => product-select/index.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename web/app/components/inputs/{product-select.hbs => product-select/index.hbs} (100%) rename web/app/components/inputs/{product-select.ts => product-select/index.ts} (100%) diff --git a/web/app/components/inputs/product-select.hbs b/web/app/components/inputs/product-select/index.hbs similarity index 100% rename from web/app/components/inputs/product-select.hbs rename to web/app/components/inputs/product-select/index.hbs diff --git a/web/app/components/inputs/product-select.ts b/web/app/components/inputs/product-select/index.ts similarity index 100% rename from web/app/components/inputs/product-select.ts rename to web/app/components/inputs/product-select/index.ts From bbdb6fdcfad8f4dcee2ff07d5a020168e22357d6 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 15:58:19 -0400 Subject: [PATCH 094/128] Update docNumber function --- web/mirage/config.ts | 5 ++--- web/mirage/factories/document.ts | 2 +- web/tests/acceptance/authenticated/document-test.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/web/mirage/config.ts b/web/mirage/config.ts index a98789634..0e87445c6 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -2,7 +2,7 @@ import { Collection, Response, createServer } from "miragejs"; import config from "../config/environment"; -import { getTestProductAbbreviation } from "./factories/document"; +import { getTestDocNumber } from "./factories/document"; export default function (mirageConfig) { let finalConfig = { @@ -308,12 +308,11 @@ export default function (mirageConfig) { let document = schema.document.findBy({ objectID: request.params.document_id, }); - if (document) { let attrs = JSON.parse(request.requestBody); if ("product" in attrs) { - attrs.docNumber = getTestProductAbbreviation(attrs.product); + attrs.docNumber = getTestDocNumber(attrs.product); } document.update(attrs); diff --git a/web/mirage/factories/document.ts b/web/mirage/factories/document.ts index 6045ee14d..32af1839d 100644 --- a/web/mirage/factories/document.ts +++ b/web/mirage/factories/document.ts @@ -11,7 +11,7 @@ export function getTestDocNumber(product: string) { abbreviation = "TP1"; break; case "Test Product 2": - abbreviation = "TP2-001"; + abbreviation = "TP2"; break; default: abbreviation = "HCP"; diff --git a/web/tests/acceptance/authenticated/document-test.ts b/web/tests/acceptance/authenticated/document-test.ts index 901d145f7..e72b3df0a 100644 --- a/web/tests/acceptance/authenticated/document-test.ts +++ b/web/tests/acceptance/authenticated/document-test.ts @@ -80,7 +80,7 @@ module("Acceptance | authenticated/document", function (hooks) { assert.equal( option.textContent?.trim(), expectedProducts[index], - "the product is correct" + "the product list item is correct" ); }); From 57516d15f9510ce9e5ed3b42fe1ead875906eb8d Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:03:09 -0400 Subject: [PATCH 095/128] Update badge-dropdown-list-test.ts --- .../components/inputs/badge-dropdown-list-test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts index f294b8068..fdf107107 100644 --- a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -1,13 +1,7 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "ember-qunit"; import { hbs } from "ember-cli-htmlbars"; -import { - click, - findAll, - render, - teardownContext, - waitUntil, -} from "@ember/test-helpers"; +import { click, findAll, render } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; import { Placement } from "@floating-ui/dom"; From c00bc4ed9a6147b0d3ae29b572a5c4d601699323 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:05:43 -0400 Subject: [PATCH 096/128] Update checkable-item.hbs --- web/app/components/x/dropdown-list/checkable-item.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index 34df78561..89baa8a81 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,3 +1,4 @@ +{{! @glint-nocheck - not typesafe yet}} Date: Tue, 30 May 2023 16:14:43 -0400 Subject: [PATCH 097/128] Update item.ts --- web/app/components/x/dropdown-list/item.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index bf578dce0..c5eb67d13 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -86,9 +86,9 @@ export default class XDropdownListItemComponent extends Component handling. - * This creates issues in the testing environment, so we + * This approach causes issues when testing, so we * use `schedule` as an approximation. * * TODO: Improve this. From 675df6da3332681b6020dc6234ce89a838d12e60 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:05:04 -0400 Subject: [PATCH 098/128] BadgeDropdownList component --- .../components/inputs/badge-dropdown-list.hbs | 51 ++++++++++++ .../components/inputs/badge-dropdown-list.ts | 26 ++++++ .../x/dropdown-list/checkable-item.hbs | 2 + .../x/dropdown-list/checkable-item.ts | 6 ++ web/app/components/x/dropdown-list/item.ts | 23 ++++-- .../components/x/dropdown/list-item.scss | 54 +++++++++++-- web/app/styles/hds-overrides.scss | 8 ++ .../inputs/badge-dropdown-list-test.ts | 80 +++++++++++++++++++ 8 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 web/app/components/inputs/badge-dropdown-list.hbs create mode 100644 web/app/components/inputs/badge-dropdown-list.ts create mode 100644 web/tests/integration/components/inputs/badge-dropdown-list-test.ts diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs new file mode 100644 index 000000000..9d06689f1 --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -0,0 +1,51 @@ +{{! @glint-nocheck - not typesafe yet }} + + <:anchor as |dd|> +
    + {{#if @isSaving}} +
    + +
    + {{/if}} + + + + +
    + + <:item as |dd|> + {{#if (has-block "item")}} + {{yield dd to="item"}} + {{else}} + + + + {{/if}} + +
    diff --git a/web/app/components/inputs/badge-dropdown-list.ts b/web/app/components/inputs/badge-dropdown-list.ts new file mode 100644 index 000000000..aa9234ce1 --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.ts @@ -0,0 +1,26 @@ +import { Placement } from "@floating-ui/dom"; +import Component from "@glimmer/component"; + +interface InputsBadgeDropdownListComponentSignature { + Element: HTMLDivElement; + Args: { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: ((e: Event) => void) | ((e: string) => void); + placement?: Placement; + }; + Blocks: { + default: []; + item: [dd: any]; + }; +} + +export default class InputsBadgeDropdownListComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::BadgeDropdownList": typeof InputsBadgeDropdownListComponent; + } +} diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index 8717250cd..a7a0fae37 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,5 +1,7 @@ +{{! @glint-nocheck - not typesafe yet}} diff --git a/web/app/components/x/dropdown-list/checkable-item.ts b/web/app/components/x/dropdown-list/checkable-item.ts index 82061869d..418466ab9 100644 --- a/web/app/components/x/dropdown-list/checkable-item.ts +++ b/web/app/components/x/dropdown-list/checkable-item.ts @@ -9,3 +9,9 @@ interface XDropdownListCheckableItemComponentSignature { } export default class XDropdownListCheckableItemComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "X::DropdownList::CheckableItem": typeof XDropdownListCheckableItemComponent; + } +} diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index 9f7e03fcf..c5eb67d13 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -3,7 +3,8 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; -import { next } from "@ember/runloop"; +import { next, schedule } from "@ember/runloop"; +import Ember from "ember"; interface XDropdownListItemComponentSignature { Args: { @@ -85,12 +86,22 @@ export default class XDropdownListItemComponent extends Component handling. + * In production, close the dropdown on the next run loop + * so that we don't interfere with Ember's handling. + * This approach causes issues when testing, so we + * use `schedule` as an approximation. + * + * TODO: Improve this. */ - next(() => { - this.args.hideDropdown(); - }); + if (Ember.testing) { + schedule("afterRender", () => { + this.args.hideDropdown(); + }); + } else { + next(() => { + this.args.hideDropdown(); + }); + } } /** diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index 764b8664a..787292680 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -13,16 +13,60 @@ } } + &:not(.is-aria-selected) { + .check { + @apply text-color-foreground-action; + } + } + .flight-icon { - @apply text-color-foreground-action shrink-0; + @apply shrink-0; - &.check { - @apply mr-2.5; + .x-dropdown-list-item { + @apply flex; } - &.sort-icon { - @apply ml-3 mr-4; + .x-dropdown-list-item-link { + @apply no-underline flex text-body-200 items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; + + &.is-aria-selected { + @apply bg-color-foreground-action text-color-foreground-high-contrast outline-none; + + .flight-icon { + @apply text-inherit; + } + } + + &:not(.is-aria-selected) { + .check { + @apply text-color-foreground-action; + } + } + + .flight-icon { + @apply shrink-0; + + &.check { + @apply mr-2.5; + } + + &.sort-icon { + @apply ml-3 mr-4; + } + } + } + + .x-dropdown-list-item-value { + @apply w-full truncate whitespace-nowrap; } + + .x-dropdown-list-item-count { + @apply ml-8 shrink-0; + } + } + + &.sort-icon { + @apply ml-3 mr-4; } } diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index 70b0a9bba..b64d0ff37 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -30,3 +30,11 @@ } } } + +.hds-badge-dropdown { + @apply pr-6; + + + .dropdown-caret { + @apply absolute right-1.5 top-1/2 -translate-y-1/2; + } +} diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts new file mode 100644 index 000000000..fdf107107 --- /dev/null +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -0,0 +1,80 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { click, findAll, render } from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; +import { Placement } from "@floating-ui/dom"; + +interface BadgeDropdownListTestContext extends MirageTestContext { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: ((e: Event) => void) | ((selected: string) => void); + placement?: Placement; +} + +module( + "Integration | Component | inputs/badge-dropdown-list", + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) { + this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; + this.selected = Object.keys(this.items)[1]; + this.onItemClick = (selected: string) => { + this.set("selected", selected); + }; + + await render(hbs` + {{! @glint-ignore: not typed yet }} + + `); + + const iconSelector = "[data-test-badge-dropdown-list-icon] .flight-icon"; + const triggerSelector = "[data-test-badge-dropdown-trigger]"; + const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]"; + const itemSelector = "[data-test-x-dropdown-list-item]"; + const itemActionSelector = + "[data-test-badge-dropdown-list-default-action]"; + + assert.dom(iconSelector).hasAttribute("data-test-icon", "folder"); + assert.dom(triggerSelector).hasText("Labs"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + + await click(triggerSelector); + + let listItemsText = findAll(itemActionSelector).map((el) => + el.textContent?.trim() + ); + + assert.deepEqual( + listItemsText, + ["Waypoint", "Labs", "Boundary"], + "correct list items are rendered" + ); + + assert + .dom( + `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` + ) + .hasAttribute("data-test-is-checked"); + + await click(itemActionSelector); + + assert.dom(triggerSelector).hasText("Waypoint"); + assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + }); + } +); From 9699c597871995c169fdfe9a76cf5ab5dd289288 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:05:04 -0400 Subject: [PATCH 099/128] BadgeDropdownList component --- .../components/inputs/badge-dropdown-list.hbs | 51 ++++++++++++ .../components/inputs/badge-dropdown-list.ts | 26 ++++++ .../x/dropdown-list/checkable-item.hbs | 2 + .../x/dropdown-list/checkable-item.ts | 6 ++ web/app/components/x/dropdown-list/item.ts | 23 ++++-- .../components/x/dropdown/list-item.scss | 50 ++++++++++- web/app/styles/hds-overrides.scss | 8 ++ .../inputs/badge-dropdown-list-test.ts | 82 +++++++++++++++++++ 8 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 web/app/components/inputs/badge-dropdown-list.hbs create mode 100644 web/app/components/inputs/badge-dropdown-list.ts create mode 100644 web/tests/integration/components/inputs/badge-dropdown-list-test.ts diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs new file mode 100644 index 000000000..9d06689f1 --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -0,0 +1,51 @@ +{{! @glint-nocheck - not typesafe yet }} + + <:anchor as |dd|> +
    + {{#if @isSaving}} +
    + +
    + {{/if}} + + + + +
    + + <:item as |dd|> + {{#if (has-block "item")}} + {{yield dd to="item"}} + {{else}} + + + + {{/if}} + +
    diff --git a/web/app/components/inputs/badge-dropdown-list.ts b/web/app/components/inputs/badge-dropdown-list.ts new file mode 100644 index 000000000..aa9234ce1 --- /dev/null +++ b/web/app/components/inputs/badge-dropdown-list.ts @@ -0,0 +1,26 @@ +import { Placement } from "@floating-ui/dom"; +import Component from "@glimmer/component"; + +interface InputsBadgeDropdownListComponentSignature { + Element: HTMLDivElement; + Args: { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: ((e: Event) => void) | ((e: string) => void); + placement?: Placement; + }; + Blocks: { + default: []; + item: [dd: any]; + }; +} + +export default class InputsBadgeDropdownListComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::BadgeDropdownList": typeof InputsBadgeDropdownListComponent; + } +} diff --git a/web/app/components/x/dropdown-list/checkable-item.hbs b/web/app/components/x/dropdown-list/checkable-item.hbs index 8717250cd..a7a0fae37 100644 --- a/web/app/components/x/dropdown-list/checkable-item.hbs +++ b/web/app/components/x/dropdown-list/checkable-item.hbs @@ -1,5 +1,7 @@ +{{! @glint-nocheck - not typesafe yet}} diff --git a/web/app/components/x/dropdown-list/checkable-item.ts b/web/app/components/x/dropdown-list/checkable-item.ts index 82061869d..418466ab9 100644 --- a/web/app/components/x/dropdown-list/checkable-item.ts +++ b/web/app/components/x/dropdown-list/checkable-item.ts @@ -9,3 +9,9 @@ interface XDropdownListCheckableItemComponentSignature { } export default class XDropdownListCheckableItemComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "X::DropdownList::CheckableItem": typeof XDropdownListCheckableItemComponent; + } +} diff --git a/web/app/components/x/dropdown-list/item.ts b/web/app/components/x/dropdown-list/item.ts index 9f7e03fcf..c5eb67d13 100644 --- a/web/app/components/x/dropdown-list/item.ts +++ b/web/app/components/x/dropdown-list/item.ts @@ -3,7 +3,8 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { FocusDirection } from "."; -import { next } from "@ember/runloop"; +import { next, schedule } from "@ember/runloop"; +import Ember from "ember"; interface XDropdownListItemComponentSignature { Args: { @@ -85,12 +86,22 @@ export default class XDropdownListItemComponent extends Component handling. + * In production, close the dropdown on the next run loop + * so that we don't interfere with Ember's handling. + * This approach causes issues when testing, so we + * use `schedule` as an approximation. + * + * TODO: Improve this. */ - next(() => { - this.args.hideDropdown(); - }); + if (Ember.testing) { + schedule("afterRender", () => { + this.args.hideDropdown(); + }); + } else { + next(() => { + this.args.hideDropdown(); + }); + } } /** diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index 764b8664a..d03575a5b 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -13,16 +13,60 @@ } } + &:not(.is-aria-selected) { + .check { + @apply text-color-foreground-action; + } + } + .flight-icon { - @apply text-color-foreground-action shrink-0; + @apply shrink-0; &.check { @apply mr-2.5; } - &.sort-icon { - @apply ml-3 mr-4; + .x-dropdown-list-item { + @apply flex; + } + + .x-dropdown-list-item-link { + @apply no-underline flex text-body-200 items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; + + &.is-aria-selected { + @apply bg-color-foreground-action text-color-foreground-high-contrast outline-none; + + .flight-icon { + @apply text-inherit; + } + } + + &:not(.is-aria-selected) { + .check { + @apply text-color-foreground-action; + } + } + + .flight-icon { + @apply shrink-0; + + &.sort-icon { + @apply ml-3 mr-4; + } + } + } + + .x-dropdown-list-item-value { + @apply w-full truncate whitespace-nowrap; } + + .x-dropdown-list-item-count { + @apply ml-8 shrink-0; + } + } + + &.sort-icon { + @apply ml-3 mr-4; } } diff --git a/web/app/styles/hds-overrides.scss b/web/app/styles/hds-overrides.scss index 70b0a9bba..b64d0ff37 100644 --- a/web/app/styles/hds-overrides.scss +++ b/web/app/styles/hds-overrides.scss @@ -30,3 +30,11 @@ } } } + +.hds-badge-dropdown { + @apply pr-6; + + + .dropdown-caret { + @apply absolute right-1.5 top-1/2 -translate-y-1/2; + } +} diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts new file mode 100644 index 000000000..99d713550 --- /dev/null +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -0,0 +1,82 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { click, findAll, render } from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; +import { Placement } from "@floating-ui/dom"; + +interface BadgeDropdownListTestContext extends MirageTestContext { + items: any; + selected?: any; + listIsOrdered?: boolean; + isSaving?: boolean; + onItemClick: ((e: Event) => void) | ((selected: string) => void); + placement?: Placement; +} + +module( + "Integration | Component | inputs/badge-dropdown-list", + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) { + this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; + this.selected = Object.keys(this.items)[1]; + this.onItemClick = (selected: string) => { + this.set("selected", selected); + }; + + await render(hbs` + {{! @glint-ignore: not typed yet }} + + `); + + const iconSelector = "[data-test-badge-dropdown-list-icon] .flight-icon"; + const triggerSelector = "[data-test-badge-dropdown-trigger]"; + const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]"; + const itemSelector = "[data-test-x-dropdown-list-item]"; + const itemActionSelector = + "[data-test-badge-dropdown-list-default-action]"; + + assert.dom(iconSelector).hasAttribute("data-test-icon", "folder"); + assert.dom(triggerSelector).hasText("Labs"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + + await click(triggerSelector); + + await this.pauseTest(); + + let listItemsText = findAll(itemActionSelector).map((el) => + el.textContent?.trim() + ); + + assert.deepEqual( + listItemsText, + ["Waypoint", "Labs", "Boundary"], + "correct list items are rendered" + ); + + assert + .dom( + `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` + ) + .hasAttribute("data-test-is-checked"); + + await click(itemActionSelector); + + assert.dom(triggerSelector).hasText("Waypoint"); + assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); + assert + .dom(chevronSelector) + .hasAttribute("data-test-chevron-position", "down"); + }); + } +); From a8db64b7de6f8431d56e2c81a6f0a38a63ea6963 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:33:19 -0400 Subject: [PATCH 100/128] Add custom-item test --- .../inputs/badge-dropdown-list-test.ts | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts index 99d713550..20b0bd414 100644 --- a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -15,19 +15,26 @@ interface BadgeDropdownListTestContext extends MirageTestContext { placement?: Placement; } +const TRIGGER_SELECTOR = "[data-test-badge-dropdown-trigger]"; +const ITEM_SELECTOR = "[data-test-x-dropdown-list-item]"; +const DEFAULT_ACTION_SELECTOR = + "[data-test-badge-dropdown-list-default-action]"; + module( "Integration | Component | inputs/badge-dropdown-list", function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); - test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) { + hooks.beforeEach(function (this: BadgeDropdownListTestContext) { this.items = { Waypoint: {}, Labs: {}, Boundary: {} }; this.selected = Object.keys(this.items)[1]; this.onItemClick = (selected: string) => { this.set("selected", selected); }; + }); + test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) { await render(hbs` {{! @glint-ignore: not typed yet }} + let listItemsText = findAll(DEFAULT_ACTION_SELECTOR).map((el) => el.textContent?.trim() ); @@ -66,17 +67,48 @@ module( assert .dom( - `${itemSelector}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` + `${ITEM_SELECTOR}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]` ) .hasAttribute("data-test-is-checked"); - await click(itemActionSelector); + await click(DEFAULT_ACTION_SELECTOR); - assert.dom(triggerSelector).hasText("Waypoint"); + assert.dom(TRIGGER_SELECTOR).hasText("Waypoint"); assert.dom(iconSelector).hasAttribute("data-test-icon", "waypoint"); assert .dom(chevronSelector) .hasAttribute("data-test-chevron-position", "down"); }); + + test("it functions as expected (custom interactive item)", async function (this: BadgeDropdownListTestContext, assert) { + await render(hbs` + {{! @glint-ignore: not typed yet }} + + <:item as |dd|> + + {{dd.value}} + {{#if dd.selected}} + (selected) + {{/if}} + + + + `); + + await click(TRIGGER_SELECTOR); + + assert.dom(ITEM_SELECTOR).hasText("Waypoint"); + + assert + .dom(DEFAULT_ACTION_SELECTOR) + .doesNotExist("default action is not rendered"); + + // assert that the second item is selected + assert.dom(`${ITEM_SELECTOR}:nth-child(2)`).hasText("Labs (selected)"); + }); } ); From 506b825aa62a33be6eebbd8532dc0f87431b403b Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:34:34 -0400 Subject: [PATCH 101/128] Remove unnecessary comment --- .../integration/components/inputs/badge-dropdown-list-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts index 20b0bd414..27d059c10 100644 --- a/web/tests/integration/components/inputs/badge-dropdown-list-test.ts +++ b/web/tests/integration/components/inputs/badge-dropdown-list-test.ts @@ -107,7 +107,6 @@ module( .dom(DEFAULT_ACTION_SELECTOR) .doesNotExist("default action is not rendered"); - // assert that the second item is selected assert.dom(`${ITEM_SELECTOR}:nth-child(2)`).hasText("Labs (selected)"); }); } From df1e9a9fe3bb022bf0086aec51dc86b7166d193c Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:17:52 -0400 Subject: [PATCH 102/128] Update list-item.scss --- web/app/styles/components/x/dropdown/list-item.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index 210ca5163..e195f5788 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -22,9 +22,17 @@ .flight-icon { @apply shrink-0; - &.sort-icon { - @apply ml-3 mr-4; + &.check { + @apply mr-2.5; } + + .x-dropdown-list-item { + @apply flex; + } + } + + &.sort-icon { + @apply ml-3 mr-4; } } From ffe0d6efe63d81511d53241309e77f45e2ea02df Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:39:35 -0400 Subject: [PATCH 103/128] Resolve merge conflict --- .../components/x/dropdown/list-item.scss | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index 795146e93..e195f5788 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -22,47 +22,13 @@ .flight-icon { @apply shrink-0; - .x-dropdown-list-item { - @apply flex; + &.check { + @apply mr-2.5; } .x-dropdown-list-item { @apply flex; } - - .x-dropdown-list-item-link { - @apply no-underline flex text-body-200 items-center py-[7px] pl-2.5 pr-8 w-full text-color-foreground-primary; - - &.is-aria-selected { - @apply bg-color-foreground-action text-color-foreground-high-contrast outline-none; - - .flight-icon { - @apply text-inherit; - } - } - - &:not(.is-aria-selected) { - .check { - @apply text-color-foreground-action; - } - } - - .flight-icon { - @apply shrink-0; - - &.sort-icon { - @apply ml-3 mr-4; - } - } - } - - .x-dropdown-list-item-value { - @apply w-full truncate whitespace-nowrap; - } - - .x-dropdown-list-item-count { - @apply ml-8 shrink-0; - } } &.sort-icon { From e84f45455c924fcaddf5c0bba5f435ad2ec45d6b Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 16:55:06 -0400 Subject: [PATCH 104/128] ProductSelect component --- .../inputs/product-select/index.hbs | 76 +++++++++++++++++++ .../components/inputs/product-select/index.ts | 62 +++++++++++++++ .../components/inputs/product-select/item.hbs | 18 +++++ .../components/inputs/product-select/item.ts | 17 +++++ web/app/styles/app.scss | 2 +- .../components/x/dropdown/toggle-select.scss | 22 ++++++ web/app/types/hds-badge.d.ts | 5 ++ 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 web/app/components/inputs/product-select/index.hbs create mode 100644 web/app/components/inputs/product-select/index.ts create mode 100644 web/app/components/inputs/product-select/item.hbs create mode 100644 web/app/components/inputs/product-select/item.ts create mode 100644 web/app/styles/components/x/dropdown/toggle-select.scss create mode 100644 web/app/types/hds-badge.d.ts diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs new file mode 100644 index 000000000..bfe63ee33 --- /dev/null +++ b/web/app/components/inputs/product-select/index.hbs @@ -0,0 +1,76 @@ +{{! @glint-nocheck - not typesafe yet }} +{{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} +
    + {{#if this.products}} + {{#if @formatIsBadge}} + + <:item as |dd|> + + + + + + {{else}} + + <:anchor as |dd|> + + + + {{or @selected "--"}} + + {{#if this.selectedProductAbbreviation}} + + {{this.selectedProductAbbreviation}} + + {{/if}} + + + + <:item as |dd|> + + + + + + {{/if}} + {{else if this.fetchProducts.isRunning}} + + {{else}} +
    + {{/if}} +
    diff --git a/web/app/components/inputs/product-select/index.ts b/web/app/components/inputs/product-select/index.ts new file mode 100644 index 000000000..e4b3b21c4 --- /dev/null +++ b/web/app/components/inputs/product-select/index.ts @@ -0,0 +1,62 @@ +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import { Placement } from "@floating-ui/dom"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { task } from "ember-concurrency"; +import FetchService from "hermes/services/fetch"; +import { BadgeSize } from "hermes/types/hds-badge"; + +interface InputsProductSelectSignature { + Element: HTMLDivElement; + Args: { + selected?: any; + onChange: (value: string) => void; + badgeSize?: BadgeSize; + formatIsBadge?: boolean; + placement?: Placement; + isSaving?: boolean; + }; +} + +type ProductAreas = { + [key: string]: { + abbreviation: string; + perDocDataType: unknown; + }; +}; + +export default class InputsProductSelectComponent extends Component { + @service("fetch") declare fetchSvc: FetchService; + + @tracked selected = this.args.selected; + + @tracked products: ProductAreas | undefined = undefined; + + get selectedProductAbbreviation() { + return this.products?.[this.selected]?.abbreviation; + } + + @action onChange(newValue: any) { + this.selected = newValue; + this.args.onChange(newValue); + } + + protected fetchProducts = task(async () => { + try { + let products = await this.fetchSvc + .fetch("/api/v1/products") + .then((resp) => resp?.json()); + this.products = products; + } catch (err) { + console.error(err); + throw err; + } + }); +} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::ProductSelect": typeof InputsProductSelectComponent; + } +} diff --git a/web/app/components/inputs/product-select/item.hbs b/web/app/components/inputs/product-select/item.hbs new file mode 100644 index 000000000..dd8d884b7 --- /dev/null +++ b/web/app/components/inputs/product-select/item.hbs @@ -0,0 +1,18 @@ +{{! @glint-nocheck - not typesafe yet }} +
    + + + {{@product}} + + {{#if @abbreviation}} + + {{@abbreviation}} + + {{/if}} + {{#if @selected}} + + {{/if}} +
    diff --git a/web/app/components/inputs/product-select/item.ts b/web/app/components/inputs/product-select/item.ts new file mode 100644 index 000000000..e64179cc9 --- /dev/null +++ b/web/app/components/inputs/product-select/item.ts @@ -0,0 +1,17 @@ +import Component from "@glimmer/component"; + +interface InputsProductSelectItemComponentSignature { + Args: { + product: string; + selected: boolean; + abbreviation?: boolean; + }; +} + +export default class InputsProductSelectItemComponent extends Component {} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Inputs::ProductSelect::Item": typeof InputsProductSelectItemComponent; + } +} diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 29e5e9283..c8910fb67 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -7,6 +7,7 @@ @use "components/x-hds-tab"; @use "components/x/dropdown/list"; @use "components/x/dropdown/list-item"; +@use "components/x/dropdown/toggle-select"; @use "components/editable-field"; @use "components/modal-dialog"; @use "components/multiselect"; @@ -24,7 +25,6 @@ @use "components/notification"; @use "components/sidebar"; @use "components/hds-badge"; -@use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; @use "hashicorp/product-badge"; diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss new file mode 100644 index 000000000..91b1aea1d --- /dev/null +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -0,0 +1,22 @@ +.x-dropdown-list-toggle-select { + @apply text-left; + + &.hds-button { + @apply justify-start; + } + + &.hds-button--size-medium { + @apply pl-2.5 pr-1.5; + } + .hds-button__icon + .hds-button__text { + @apply ml-2.5; + } + + &.hds-button--color-secondary:not(:hover) { + @apply bg-color-surface-primary; + + .caret { + @apply text-color-foreground-faint; + } + } +} diff --git a/web/app/types/hds-badge.d.ts b/web/app/types/hds-badge.d.ts new file mode 100644 index 000000000..9c4ccab12 --- /dev/null +++ b/web/app/types/hds-badge.d.ts @@ -0,0 +1,5 @@ +export enum BadgeSize { + Small = "small", + Medium = "medium", + Large = "large", +} From 0c95dc85a8f271826419b4df9391a91efa00e2f4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 17:10:19 -0400 Subject: [PATCH 105/128] Add list item tests --- .../components/inputs/product-select/item.hbs | 15 +++-- .../inputs/product-select/item-test.ts | 66 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 web/tests/integration/components/inputs/product-select/item-test.ts diff --git a/web/app/components/inputs/product-select/item.hbs b/web/app/components/inputs/product-select/item.hbs index dd8d884b7..58d19c660 100644 --- a/web/app/components/inputs/product-select/item.hbs +++ b/web/app/components/inputs/product-select/item.hbs @@ -1,16 +1,23 @@ {{! @glint-nocheck - not typesafe yet }} -
    - - +
    + + {{@product}} {{#if @abbreviation}} - + {{@abbreviation}} {{/if}} {{#if @selected}} diff --git a/web/tests/integration/components/inputs/product-select/item-test.ts b/web/tests/integration/components/inputs/product-select/item-test.ts new file mode 100644 index 000000000..f2dc75153 --- /dev/null +++ b/web/tests/integration/components/inputs/product-select/item-test.ts @@ -0,0 +1,66 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { render } from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; + +interface InputsProductSelectItemContext extends MirageTestContext { + product: string; + selected: boolean; + abbreviation?: boolean; +} + +module( + "Integration | Component | inputs/product-select/item", + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test("it functions as expected", async function (this: InputsProductSelectItemContext, assert) { + this.set("product", "Vault"); + this.set("selected", false); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + + // assert that the icon has the "data-test-icon="vault" attribute + assert + .dom("[data-test-product-select-item-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"); + + this.set("product", "Engineering"); + this.set("selected", true); + + assert + .dom("[data-test-product-select-item-icon]") + .hasAttribute( + "data-test-icon", + "folder", + "the correct product icon is shown" + ); + + }); + } +); From 20aada4821e1ca7eaaae40e1fc656148f734150f Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 20:12:22 -0400 Subject: [PATCH 106/128] Tests, styles, cleanup --- .../components/inputs/badge-dropdown-list.hbs | 2 +- .../inputs/product-select/index.hbs | 13 +- .../components/inputs/product-select/index.ts | 21 ++- .../components/inputs/product-select/item.hbs | 3 +- .../components/inputs/product-select/item.ts | 1 + .../components/x/dropdown/toggle-select.scss | 7 +- web/app/utils/get-product-id.ts | 8 +- web/mirage/config.ts | 55 ++++---- .../inputs/product-select/index-test.ts | 126 ++++++++++++++++++ 9 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 web/tests/integration/components/inputs/product-select/index-test.ts diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index 9d06689f1..e45f0a12f 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -7,7 +7,7 @@ ...attributes > <:anchor as |dd|> -
    +
    {{#if @isSaving}}
    <:anchor as |dd|> - + + {{or @selected "--"}} {{#if this.selectedProductAbbreviation}} {{this.selectedProductAbbreviation}} {{/if}} + {{/if}} {{else if this.fetchProducts.isRunning}} - + {{else}}
    void; badgeSize?: BadgeSize; formatIsBadge?: boolean; @@ -20,10 +21,12 @@ interface InputsProductSelectSignature { } type ProductAreas = { - [key: string]: { - abbreviation: string; - perDocDataType: unknown; - }; + [key: string]: ProductArea; +}; + +type ProductArea = { + abbreviation: string; + perDocDataType: unknown; }; export default class InputsProductSelectComponent extends Component { @@ -33,8 +36,12 @@ export default class InputsProductSelectComponent extends Component +
    diff --git a/web/app/components/inputs/product-select/item.ts b/web/app/components/inputs/product-select/item.ts index e64179cc9..88202487e 100644 --- a/web/app/components/inputs/product-select/item.ts +++ b/web/app/components/inputs/product-select/item.ts @@ -1,6 +1,7 @@ import Component from "@glimmer/component"; interface InputsProductSelectItemComponentSignature { + Element: HTMLDivElement; Args: { product: string; selected: boolean; diff --git a/web/app/styles/components/x/dropdown/toggle-select.scss b/web/app/styles/components/x/dropdown/toggle-select.scss index 91b1aea1d..bf375ce47 100644 --- a/web/app/styles/components/x/dropdown/toggle-select.scss +++ b/web/app/styles/components/x/dropdown/toggle-select.scss @@ -1,12 +1,17 @@ .x-dropdown-list-toggle-select { @apply text-left; + .flight-icon { + @apply shrink-0; + } + &.hds-button { @apply justify-start; } &.hds-button--size-medium { - @apply pl-2.5 pr-1.5; + @apply px-2.5; + } .hds-button__icon + .hds-button__text { @apply ml-2.5; diff --git a/web/app/utils/get-product-id.ts b/web/app/utils/get-product-id.ts index 748cfc5dd..d3b421713 100644 --- a/web/app/utils/get-product-id.ts +++ b/web/app/utils/get-product-id.ts @@ -1,5 +1,11 @@ -export default function getProductId(productName: string): string | null { +export default function getProductId( + productName: string | null +): string | null { + if (!productName) { + return null; + } let product = productName.toLowerCase(); + switch (product) { case "boundary": case "consul": diff --git a/web/mirage/config.ts b/web/mirage/config.ts index a6456118a..2981c4529 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -228,34 +228,43 @@ export default function (mirageConfig) { * Used by the sidebar to populate a draft's product/area dropdown. */ this.get("/products", () => { - let objects = this.schema.products.all().models.map((product) => { - return { - [product.attrs.name]: { - abbreviation: product.attrs.abbreviation, - }, - }; - }); + let currentProducts = this.schema.products.all().models; + if (currentProducts.length === 0) { + return new Response( + 200, + {}, + { "Default Fetched Product": { abbreviation: "NONE" } } + ); + } else { + let objects = this.schema.products.all().models.map((product) => { + return { + [product.attrs.name]: { + abbreviation: product.attrs.abbreviation, + }, + }; + }); - // The objects currently look like: - // [ - // 0: { "Labs": { abbreviation: "LAB" } }, - // 1: { "Vault": { abbreviation: "VLT"} } - // ] + // The objects currently look like: + // [ + // 0: { "Labs": { abbreviation: "LAB" } }, + // 1: { "Vault": { abbreviation: "VLT"} } + // ] - // We reformat them to match the API's response: - // { - // "Labs": { abbreviation: "LAB" }, - // "Vault": { abbreviation: "VLT" } - // } + // We reformat them to match the API's response: + // { + // "Labs": { abbreviation: "LAB" }, + // "Vault": { abbreviation: "VLT" } + // } - let formattedObjects = {}; + let formattedObjects = {}; - objects.forEach((object) => { - let key = Object.keys(object)[0]; - formattedObjects[key] = object[key]; - }); + objects.forEach((object) => { + let key = Object.keys(object)[0]; + formattedObjects[key] = object[key]; + }); - return new Response(200, {}, formattedObjects); + return new Response(200, {}, formattedObjects); + } }); // RecentlyViewedDocsService / fetchIndexID diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select/index-test.ts new file mode 100644 index 000000000..b643bee8d --- /dev/null +++ b/web/tests/integration/components/inputs/product-select/index-test.ts @@ -0,0 +1,126 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { click, render, waitFor } from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; +import { BadgeSize } from "hermes/types/hds-badge"; +import { Placement } from "@floating-ui/dom"; + +const DEFAULT_DROPDOWN_SELECTOR = + "[data-test-product-select-default-dropdown-toggle]"; + +const LIST_ITEM_SELECTOR = "[data-test-product-select-item]"; + +interface InputsProductSelectContext extends MirageTestContext { + selected?: any; + onChange: (value: string) => void; + badgeSize?: BadgeSize; + formatIsBadge?: boolean; + placement?: Placement; + isSaving?: boolean; +} + +module("Integration | Component | inputs/product-select", function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function (this: InputsProductSelectContext) { + this.server.createList("product", 3); + this.set("selected", "Vault"); + this.set("onChange", () => {}); + }); + + test("it can render in two formats", async function (this: InputsProductSelectContext, assert) { + const badgeDropdownSelector = "[data-test-badge-dropdown-list]"; + + this.set("formatIsBadge", true); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + + assert.dom(badgeDropdownSelector).exists("badge dropdown is rendered"); + assert + .dom(DEFAULT_DROPDOWN_SELECTOR) + .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` + {{! @glint-nocheck: not typesafe yet }} + + `); + + assert + .dom("[data-test-product-select-toggle-abbreviation]") + .hasText("TST-0"); + }); + + test("it shows an empty state when nothing is selected (default toggle)", async function (this: InputsProductSelectContext, assert) { + this.set("selected", undefined); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + assert.dom("[data-test-product-select-selected-value]").hasText("--"); + }); + + test("it displays the products in a dropdown list with abbreviations", async function (this: InputsProductSelectContext, assert) { + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + + await click(DEFAULT_DROPDOWN_SELECTOR); + + assert.dom(LIST_ITEM_SELECTOR).exists({ count: 3 }); + + let firstListItem = this.element.querySelector(LIST_ITEM_SELECTOR); + assert.dom(firstListItem).hasText("Test Product 0 TST-0"); + }); + + test("it fetches the products if they aren't already loaded", async function (this: InputsProductSelectContext, assert) { + this.server.db.emptyData(); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + + await click(DEFAULT_DROPDOWN_SELECTOR); + + // In Mirage, when there are no products, we return a single default product + assert.dom(LIST_ITEM_SELECTOR).exists({ count: 1 }); + assert.dom(LIST_ITEM_SELECTOR).hasText("Default Fetched Product NONE"); + }); +}); From bb007563a1cd275016691051cbd566f10b4a80c1 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 20:15:48 -0400 Subject: [PATCH 107/128] Add onChange test --- .../inputs/product-select/index-test.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select/index-test.ts index b643bee8d..4fc8525df 100644 --- a/web/tests/integration/components/inputs/product-select/index-test.ts +++ b/web/tests/integration/components/inputs/product-select/index-test.ts @@ -119,8 +119,29 @@ module("Integration | Component | inputs/product-select", function (hooks) { await click(DEFAULT_DROPDOWN_SELECTOR); - // In Mirage, when there are no products, we return a single default product + // 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"); }); + + test("it performs the passed-in action on click", async function (this: InputsProductSelectContext, assert) { + let count = 0; + this.set("onChange", () => { + count++; + }); + + await render(hbs` + {{! @glint-nocheck: not typesafe yet }} + + `); + + await click(DEFAULT_DROPDOWN_SELECTOR); + await click(LIST_ITEM_SELECTOR); + + assert.equal(count, 1, "the action was called once"); + }); }); From 78b5a5451f9985b808d67f981b7dff4d0d93a794 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 21:40:46 -0400 Subject: [PATCH 108/128] Pass @icon into component --- web/app/components/inputs/product-select/index.hbs | 1 + web/app/components/inputs/product-select/index.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 3ebc0f8f4..207ebbdd6 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -10,6 +10,7 @@ @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} + @icon={{this.icon}} class="w-80" ...attributes > diff --git a/web/app/components/inputs/product-select/index.ts b/web/app/components/inputs/product-select/index.ts index bf4ae1bd4..b326b22a2 100644 --- a/web/app/components/inputs/product-select/index.ts +++ b/web/app/components/inputs/product-select/index.ts @@ -7,6 +7,7 @@ import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; import { BadgeSize } from "hermes/types/hds-badge"; +import getProductId from "hermes/utils/get-product-id"; interface InputsProductSelectSignature { Element: HTMLDivElement; @@ -36,6 +37,14 @@ export default class InputsProductSelectComponent extends Component Date: Tue, 30 May 2023 21:44:09 -0400 Subject: [PATCH 109/128] Remove unused property --- web/app/components/inputs/product-select/index.ts | 2 -- web/app/styles/app.scss | 1 + web/app/types/hds-badge.d.ts | 5 ----- .../components/inputs/product-select/index-test.ts | 2 -- 4 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 web/app/types/hds-badge.d.ts diff --git a/web/app/components/inputs/product-select/index.ts b/web/app/components/inputs/product-select/index.ts index bf4ae1bd4..f4e3fc345 100644 --- a/web/app/components/inputs/product-select/index.ts +++ b/web/app/components/inputs/product-select/index.ts @@ -6,14 +6,12 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; -import { BadgeSize } from "hermes/types/hds-badge"; interface InputsProductSelectSignature { Element: HTMLDivElement; Args: { selected?: string; onChange: (value: string) => void; - badgeSize?: BadgeSize; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 3792d63ad..ec5f58e85 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -27,6 +27,7 @@ @use "components/notification"; @use "components/sidebar"; @use "components/hds-badge"; +@use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; @use "hashicorp/product-badge"; diff --git a/web/app/types/hds-badge.d.ts b/web/app/types/hds-badge.d.ts deleted file mode 100644 index 9c4ccab12..000000000 --- a/web/app/types/hds-badge.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum BadgeSize { - Small = "small", - Medium = "medium", - Large = "large", -} diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select/index-test.ts index 4fc8525df..fa9181a91 100644 --- a/web/tests/integration/components/inputs/product-select/index-test.ts +++ b/web/tests/integration/components/inputs/product-select/index-test.ts @@ -4,7 +4,6 @@ import { hbs } from "ember-cli-htmlbars"; import { click, render, waitFor } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; -import { BadgeSize } from "hermes/types/hds-badge"; import { Placement } from "@floating-ui/dom"; const DEFAULT_DROPDOWN_SELECTOR = @@ -15,7 +14,6 @@ const LIST_ITEM_SELECTOR = "[data-test-product-select-item]"; interface InputsProductSelectContext extends MirageTestContext { selected?: any; onChange: (value: string) => void; - badgeSize?: BadgeSize; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; From 4691a8d812766aa42d3890897ba286b89e496653 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 21:45:35 -0400 Subject: [PATCH 110/128] Remove unused property --- web/app/components/inputs/product-select/index.ts | 2 -- web/app/types/hds-badge.d.ts | 5 ----- .../components/inputs/product-select/index-test.ts | 2 -- 3 files changed, 9 deletions(-) delete mode 100644 web/app/types/hds-badge.d.ts diff --git a/web/app/components/inputs/product-select/index.ts b/web/app/components/inputs/product-select/index.ts index b326b22a2..868b73083 100644 --- a/web/app/components/inputs/product-select/index.ts +++ b/web/app/components/inputs/product-select/index.ts @@ -6,7 +6,6 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; -import { BadgeSize } from "hermes/types/hds-badge"; import getProductId from "hermes/utils/get-product-id"; interface InputsProductSelectSignature { @@ -14,7 +13,6 @@ interface InputsProductSelectSignature { Args: { selected?: string; onChange: (value: string) => void; - badgeSize?: BadgeSize; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; diff --git a/web/app/types/hds-badge.d.ts b/web/app/types/hds-badge.d.ts deleted file mode 100644 index 9c4ccab12..000000000 --- a/web/app/types/hds-badge.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum BadgeSize { - Small = "small", - Medium = "medium", - Large = "large", -} diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select/index-test.ts index 4fc8525df..fa9181a91 100644 --- a/web/tests/integration/components/inputs/product-select/index-test.ts +++ b/web/tests/integration/components/inputs/product-select/index-test.ts @@ -4,7 +4,6 @@ import { hbs } from "ember-cli-htmlbars"; import { click, render, waitFor } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; -import { BadgeSize } from "hermes/types/hds-badge"; import { Placement } from "@floating-ui/dom"; const DEFAULT_DROPDOWN_SELECTOR = @@ -15,7 +14,6 @@ const LIST_ITEM_SELECTOR = "[data-test-product-select-item]"; interface InputsProductSelectContext extends MirageTestContext { selected?: any; onChange: (value: string) => void; - badgeSize?: BadgeSize; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; From 73029bb677f8fdc9834dbccfe5e4d508ee5db0b4 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Tue, 30 May 2023 21:46:54 -0400 Subject: [PATCH 111/128] Revert unintended change --- web/app/styles/app.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index c8910fb67..2bce31354 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -25,6 +25,7 @@ @use "components/notification"; @use "components/sidebar"; @use "components/hds-badge"; +@use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; @use "hashicorp/product-badge"; From d24e3b896f9fdc3a4391a54b38c6fa0948a4c640 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 31 May 2023 10:15:47 -0400 Subject: [PATCH 112/128] Padding tweak; Prettier --- web/app/components/inputs/product-select/index.hbs | 3 ++- web/app/components/inputs/product-select/item.hbs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 207ebbdd6..1d2eae70f 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -47,6 +47,7 @@ > {{or @selected "--"}} + {{#if this.selectedProductAbbreviation}} <:item as |dd|> - + +
    + {{@product}} + {{#if @abbreviation}} {{/if}} + {{#if @selected}} Date: Wed, 31 May 2023 10:21:55 -0400 Subject: [PATCH 113/128] Revert trivial change --- web/app/components/x/dropdown-list/link-to.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/x/dropdown-list/link-to.ts b/web/app/components/x/dropdown-list/link-to.ts index d63fa24da..a33e8ddaf 100644 --- a/web/app/components/x/dropdown-list/link-to.ts +++ b/web/app/components/x/dropdown-list/link-to.ts @@ -18,7 +18,7 @@ interface XDropdownListLinkToComponentSignature { }; Blocks: { default: []; - } + }; } export default class XDropdownListLinkToComponent extends Component {} From 9894c036427979eb65c1ad01c069e546af25a8bf Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 31 May 2023 14:26:04 -0400 Subject: [PATCH 114/128] Start of working tests --- web/app/components/header/search.hbs | 7 +- web/app/components/header/search.ts | 17 ++- web/mirage/config.ts | 56 ++++++++- web/mirage/factories/document.ts | 10 +- .../components/header/search-test.ts | 119 ++++++++++++++++++ 5 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 web/tests/integration/components/header/search-test.ts diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index c3fa13131..2781f9f83 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -14,6 +14,7 @@ <:anchor as |dd|> {{! @glint-ignore - not yet registered}} {{#unless this.query}} ⌘K @@ -43,7 +45,10 @@ {{! content is placed here by the in-element helper below }}
    {{#if this.bestMatchesHeaderIsShown}} -
    +
    diff --git a/web/app/components/header/search.ts b/web/app/components/header/search.ts index 787eb3a4e..9b9bb1f2e 100644 --- a/web/app/components/header/search.ts +++ b/web/app/components/header/search.ts @@ -11,11 +11,11 @@ import { OffsetOptions } from "@floating-ui/dom"; import ConfigService from "hermes/services/config"; import { next } from "@ember/runloop"; -interface SearchResultObjects { +export interface SearchResultObjects { [key: string]: unknown | HermesDocumentObjects; } -interface HermesDocumentObjects { +export interface HermesDocumentObjects { [key: string]: HermesDocument; } @@ -160,7 +160,12 @@ export default class HeaderSearchComponent extends Component { + const requestBody = JSON.parse(request.requestBody); + const { query, hitsPerPage } = requestBody; + + let matches: []; + + if (hitsPerPage === 1) { + matches = schema.document.all().models.filter((doc) => { + return doc.attrs.product + .toLowerCase() + .includes(query.toLowerCase()); + }); + } else { + matches = schema.document.all().models.filter((doc) => { + return ( + doc.attrs.title.toLowerCase().includes(query.toLowerCase()) || + doc.attrs.product.toLowerCase().includes(query.toLowerCase()) + ); + }); + } + + let algoliaResults: Partial = { + hits: matches, + }; + + return new Response(200, {}, algoliaResults); + }; + /** * Used by the AlgoliaSearchService to query Algolia. */ this.post( `https://${config.algolia.appID}-dsn.algolia.net/1/indexes/**`, - () => { - return { - facets: [], - hits: [], - }; + (schema, request) => { + return getAlgoliaSearchResults(schema, request); } ); + /** + * Used by the global search input to query Algolia. + */ + + let algoliaSearchHosts = []; + + for (let i = 1; i <= 9; i++) { + algoliaSearchHosts.push( + `https://${config.algolia.appID}-${i}.algolianet.com/1/indexes/**` + ); + } + + algoliaSearchHosts.forEach((host) => { + this.post(host, (schema, request) => { + return getAlgoliaSearchResults(schema, request); + }); + }); + /** * Called by the Document route to log a document view. */ diff --git a/web/mirage/factories/document.ts b/web/mirage/factories/document.ts index aac19a2a3..136e5c4ae 100644 --- a/web/mirage/factories/document.ts +++ b/web/mirage/factories/document.ts @@ -7,6 +7,12 @@ export default Factory.extend({ product: "Vault", docType: "RFC", modifiedTime: 1, - docNumber: "RFC-0000", - title: "My Document", + docNumber: (i: number) => `RFC-00${i}`, + title: (i: number) => `Test Document ${i}`, + _snippetResult: { + content: { + value: "This is a test document", + }, + }, + owners: ["Test user"] }); diff --git a/web/tests/integration/components/header/search-test.ts b/web/tests/integration/components/header/search-test.ts new file mode 100644 index 000000000..6fff9536d --- /dev/null +++ b/web/tests/integration/components/header/search-test.ts @@ -0,0 +1,119 @@ +import { module, test } from "qunit"; +import { setupRenderingTest } from "ember-qunit"; +import { hbs } from "ember-cli-htmlbars"; +import { + click, + fillIn, + render, + triggerKeyEvent, + teardownContext, + triggerEvent, +} from "@ember/test-helpers"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { MirageTestContext } from "ember-cli-mirage/test-support"; + +const KEYBOARD_SHORTCUT_SELECTOR = "[data-test-search-keyboard-shortcut]"; +const SEARCH_INPUT_SELECTOR = "[data-test-global-search-input]"; +const POPOVER_SELECTOR = ".search-popover"; +const BEST_MATCHES_HEADER_SELECTOR = "[data-test-search-best-matches-header]"; +interface HeaderSearchTestContext extends MirageTestContext {} + +module("Integration | Component | header/search", function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function (this: HeaderSearchTestContext) { + this.server.createList("document", 10); + }); + + test("it renders correctly", async function (this: HeaderSearchTestContext, assert) { + await render(hbs` + + `); + + assert.dom(".test-search").exists("renders with the splatted className"); + }); + + test("it conditionally shows a keyboard shortcut icon", async function (this: HeaderSearchTestContext, assert) { + await render(hbs` + + `); + + assert + .dom(KEYBOARD_SHORTCUT_SELECTOR) + .exists("the keyboard shortcut icon is shown"); + + await fillIn(SEARCH_INPUT_SELECTOR, "test"); + + assert + .dom(KEYBOARD_SHORTCUT_SELECTOR) + .doesNotExist( + "the keyboard shortcut icon is hidden when the user enters a query" + ); + + /** + * FIXME: Investigate unresolved promises + * + * For reasons not yet clear, this test has unresolved promises + * that prevent it from completing naturally. Because of this, + * we handle teardown manually. + * + */ + // teardownContext(this); + }); + + test("it conditionally shows a popover", async function (this: HeaderSearchTestContext, assert) { + await render(hbs` + +
    + `); + + assert.dom(POPOVER_SELECTOR).doesNotExist("the popover is hidden"); + + await fillIn(SEARCH_INPUT_SELECTOR, "t"); + + assert + .dom(POPOVER_SELECTOR) + .exists("the popover is shown when a query is entered"); + + /** + * FIXME: Investigate unresolved promises + * + * For reasons not yet clear, this test has unresolved promises + * that prevent it from completing naturally. Because of this, + * we handle teardown manually. + * + */ + // teardownContext(this); + }); + + test('it conditionally shows a "best matches" header', async function (this: HeaderSearchTestContext, assert) { + // probably need to seed some products + + await render(hbs` + + `); + + await fillIn(SEARCH_INPUT_SELECTOR, "xyz"); + + assert.dom(BEST_MATCHES_HEADER_SELECTOR).doesNotExist(); + + await fillIn(SEARCH_INPUT_SELECTOR, "vault"); + + assert + .dom(BEST_MATCHES_HEADER_SELECTOR) + .exists( + 'the "best matches" header is shown when a product/area is matched' + ); + + /** + * FIXME: Investigate unresolved promises + * + * For reasons not yet clear, this test has unresolved promises + * that prevent it from completing naturally. Because of this, + * we handle teardown manually. + * + */ + // teardownContext(this); + }); +}); From f1fb58096391279e38eea9d1983bed26d7dde0fd Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 31 May 2023 15:14:08 -0400 Subject: [PATCH 115/128] More tests --- web/app/components/header/search.hbs | 6 + web/app/components/header/search.ts | 5 +- .../components/header/search-test.ts | 138 +++++++++++++----- 3 files changed, 112 insertions(+), 37 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index 2781f9f83..22777a260 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -71,6 +71,7 @@ }} {{#if (eq dd.value "viewAllResultsObject")}} {{else}}

    {{dd.attrs.title}} @@ -123,6 +127,7 @@ {{! @glint-ignore - not yet registered}} diff --git a/web/app/components/header/search.ts b/web/app/components/header/search.ts index 9b9bb1f2e..45dddaf4b 100644 --- a/web/app/components/header/search.ts +++ b/web/app/components/header/search.ts @@ -95,7 +95,7 @@ export default class HeaderSearchComponent extends Component

    {{else}} diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index e0b68e9a0..3dd54dea0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -44,7 +44,6 @@ export default class FloatingUIContent extends Component { this.content.setAttribute("data-floating-ui-placement", placement); - Object.assign(this.content.style, { left: `${x}px`, top: `${y}px`, diff --git a/web/app/components/inputs/badge-dropdown-list.hbs b/web/app/components/inputs/badge-dropdown-list.hbs index 0fcde9732..1dcf423c7 100644 --- a/web/app/components/inputs/badge-dropdown-list.hbs +++ b/web/app/components/inputs/badge-dropdown-list.hbs @@ -4,6 +4,7 @@ @listIsOrdered={{@listIsOrdered}} @onItemClick={{@onItemClick}} @selected={{@selected}} + @placement={{@placement}} ...attributes > <:anchor as |dd|> diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 1d2eae70f..2559bd38b 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -31,7 +31,7 @@ @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} - class="w-[300px] max-h-[305px]" + class="w-[300px] max-h-[245px]" ...attributes > <:anchor as |dd|> diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index 4dea5aa3a..884332108 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -73,7 +73,7 @@
    diff --git a/web/app/styles/components/x/dropdown/list-item.scss b/web/app/styles/components/x/dropdown/list-item.scss index e195f5788..6cac11852 100644 --- a/web/app/styles/components/x/dropdown/list-item.scss +++ b/web/app/styles/components/x/dropdown/list-item.scss @@ -22,10 +22,6 @@ .flight-icon { @apply shrink-0; - &.check { - @apply mr-2.5; - } - .x-dropdown-list-item { @apply flex; } From b360b9e0dd62ce2d493fc3bd368b2ed0cb187656 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 2 Jun 2023 15:24:42 -0400 Subject: [PATCH 118/128] Cleanup --- web/app/components/header/nav.hbs | 2 +- web/app/components/header/search.hbs | 50 ++++++------------- web/app/styles/components/header/search.scss | 26 +++++++++- web/app/styles/components/nav.scss | 2 +- .../integration/components/header/nav-test.ts | 2 +- .../components/header/search-test.ts | 17 ++----- 6 files changed, 47 insertions(+), 52 deletions(-) diff --git a/web/app/components/header/nav.hbs b/web/app/components/header/nav.hbs index 831376ed1..966f82cbe 100644 --- a/web/app/components/header/nav.hbs +++ b/web/app/components/header/nav.hbs @@ -33,7 +33,7 @@
    - +
    - {{! @glint-ignore - not yet registered}} <:anchor as |dd|> - {{! @glint-ignore - not yet registered}} {{#unless this.query}} - + ⌘K {{/unless}} <:header> -
    +
    {{! content is placed here by the in-element helper below }}
    {{#if this.bestMatchesHeaderIsShown}} -
    -
    - Best matches -
    +
    +
    Best matches
    {{/if}} @@ -66,18 +57,16 @@ }} {{#unless this.searchInputIsEmpty}} {{#in-element - (html-element "#hermes-search-popover-header") + (html-element "#global-search-popover-header") insertBefore=null }} {{#if (eq dd.value "viewAllResultsObject")}} - {{! @glint-ignore - not yet registered}} View all results for "{{this.query}}" @@ -86,16 +75,13 @@ data-test-product-match-link @route="authenticated.all" @query={{hash product=(array dd.attrs.product) page=1}} - class="search-popover-header-link border-t border-t-color-border-primary" + class="global-search-popover-header-link border-t border-t-color-border-primary" > - {{! @glint-ignore - not yet registered}} View all - {{! @glint-ignore - not yet registered}} @@ -110,31 +96,23 @@ data-test-search-result @route="authenticated.document" @model={{dd.value}} - class="flex items-center space-x-3 py-2 px-3 h-20" + class="global-search-result" > - {{! @glint-ignore - not yet registered}} -
    -

    +
    +

    {{dd.attrs.title}}

    - - {{! @glint-ignore - not yet registered}} - {{#if dd.attrs._snippetResult.content.value}} - {{! @glint-ignore - not yet registered}} Date: Fri, 2 Jun 2023 15:36:41 -0400 Subject: [PATCH 119/128] Cleanup and height tweaks --- web/app/components/document/sidebar.hbs | 1 - web/app/components/document/sidebar.js | 17 ++++++++++------- web/app/components/floating-u-i/content.ts | 1 + .../components/inputs/product-select/index.hbs | 2 +- web/app/components/new/doc-form.hbs | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index f34565c47..6c79088fc 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -143,7 +143,6 @@ @onChange={{this.updateProduct.perform}} @isSaving={{this.save.isRunning}} @formatIsBadge={{true}} - class="max-h-[345px]" />
    {{else}} diff --git a/web/app/components/document/sidebar.js b/web/app/components/document/sidebar.js index be3345313..02f58d11c 100644 --- a/web/app/components/document/sidebar.js +++ b/web/app/components/document/sidebar.js @@ -49,6 +49,10 @@ export default class DocumentSidebar extends Component { @tracked userHasScrolled = false; @tracked body = null; + /** + * If the user is saving a new product, this will be the new product. + * Reset when the save task fails. + */ @tracked _newProduct = null; /** @@ -192,13 +196,6 @@ export default class DocumentSidebar extends Component { getOwner(this).lookup(`route:${this.router.currentRouteName}`).refresh(); } - @task({ restartable: true }) - *updateProduct(product) { - this._newProduct = product; - this.product = product; - yield this.save.perform("product", this.product); - } - @action maybeShowFlashError(error, title) { if (!this.modalIsActive) { this.flashMessages.add({ @@ -221,6 +218,12 @@ export default class DocumentSidebar extends Component { }); } + @task({ restartable: true }) + *updateProduct(product) { + this._newProduct = product; + yield this.save.perform("product", this._newProduct); + } + @task *save(field, val) { if (field && val) { diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts index 3dd54dea0..e0b68e9a0 100644 --- a/web/app/components/floating-u-i/content.ts +++ b/web/app/components/floating-u-i/content.ts @@ -44,6 +44,7 @@ export default class FloatingUIContent extends Component { this.content.setAttribute("data-floating-ui-placement", placement); + Object.assign(this.content.style, { left: `${x}px`, top: `${y}px`, diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 2559bd38b..cc5d517b3 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -11,7 +11,7 @@ @placement={{@placement}} @isSaving={{@isSaving}} @icon={{this.icon}} - class="w-80" + class="w-80 max-h-[217px]" ...attributes > <:item as |dd|> diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index 884332108..4dea5aa3a 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -73,7 +73,7 @@

    From 0ae754f3d72c944ade3cda3d17430e86a377553f Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Fri, 2 Jun 2023 15:51:36 -0400 Subject: [PATCH 120/128] Class cleanup --- .../inputs/product-select/index.hbs | 22 +++++-------------- web/app/components/new/doc-form.hbs | 4 ++-- web/app/styles/app.scss | 1 + .../inputs/product-select/index.scss | 19 ++++++++++++++++ web/app/styles/typography.scss | 8 +++++++ .../inputs/product-select/index-test.ts | 12 ++++------ 6 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 web/app/styles/components/inputs/product-select/index.scss diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index cc5d517b3..e8c43ee7c 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -11,7 +11,7 @@ @placement={{@placement}} @isSaving={{@isSaving}} @icon={{this.icon}} - class="w-80 max-h-[217px]" + class="w-80 product-select-dropdown-list" ...attributes > <:item as |dd|> @@ -31,36 +31,26 @@ @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} - class="w-[300px] max-h-[245px]" + class="w-[300px] product-select-dropdown-list" ...attributes > <:anchor as |dd|> - + {{or @selected "--"}} {{#if this.selectedProductAbbreviation}} - + {{this.selectedProductAbbreviation}} {{/if}} - + <:item as |dd|> diff --git a/web/app/components/new/doc-form.hbs b/web/app/components/new/doc-form.hbs index 4dea5aa3a..af0f6a5e1 100644 --- a/web/app/components/new/doc-form.hbs +++ b/web/app/components/new/doc-form.hbs @@ -1,4 +1,4 @@ -{{! @glint-nocheck - not typesafe yet }} +{{! @glint-nocheck: not typesafe yet }} {{#if this.docIsBeingCreated}}
    @@ -66,7 +66,7 @@ Product/Area   - + Where your doc should be categorized.
    diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index ec5f58e85..2c6ba8c33 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -18,6 +18,7 @@ @use "components/template-card"; @use "components/header/active-filter-list"; @use "components/header/active-filter-list-item"; +@use "components/inputs/product-select/index.scss"; @use "components/doc/tile-list"; @use "components/doc/thumbnail"; @use "components/doc/folder-affordance"; diff --git a/web/app/styles/components/inputs/product-select/index.scss b/web/app/styles/components/inputs/product-select/index.scss new file mode 100644 index 000000000..c2b6f8654 --- /dev/null +++ b/web/app/styles/components/inputs/product-select/index.scss @@ -0,0 +1,19 @@ +.product-select-dropdown-list { + @apply max-h-[217px]; +} + +.product-select-default-toggle { + @apply min-w-[250px] 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/typography.scss b/web/app/styles/typography.scss index fa7284505..05b5cc855 100644 --- a/web/app/styles/typography.scss +++ b/web/app/styles/typography.scss @@ -1,3 +1,11 @@ .hermes-form-label { @apply text-body-200 font-semibold flex text-color-foreground-strong; + + + .hermes-form-helper-text { + @apply mt-1; + } +} + +.hermes-form-helper-text { + @apply text-body-100; } diff --git a/web/tests/integration/components/inputs/product-select/index-test.ts b/web/tests/integration/components/inputs/product-select/index-test.ts index 8eeb8fd54..5aaae1631 100644 --- a/web/tests/integration/components/inputs/product-select/index-test.ts +++ b/web/tests/integration/components/inputs/product-select/index-test.ts @@ -1,14 +1,12 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "ember-qunit"; import { hbs } from "ember-cli-htmlbars"; -import { click, render, waitFor } from "@ember/test-helpers"; +import { click, render } from "@ember/test-helpers"; import { setupMirage } from "ember-cli-mirage/test-support"; import { MirageTestContext } from "ember-cli-mirage/test-support"; import { Placement } from "@floating-ui/dom"; -const DEFAULT_DROPDOWN_SELECTOR = - "[data-test-product-select-default-dropdown-toggle]"; - +const DEFAULT_DROPDOWN_SELECTOR = ".product-select-default-toggle"; const LIST_ITEM_SELECTOR = "[data-test-product-select-item]"; interface InputsProductSelectContext extends MirageTestContext { @@ -69,9 +67,7 @@ module("Integration | Component | inputs/product-select", function (hooks) { /> `); - assert - .dom("[data-test-product-select-toggle-abbreviation]") - .hasText("TP0"); + 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) { @@ -84,7 +80,7 @@ module("Integration | Component | inputs/product-select", function (hooks) { @onChange={{this.onChange}} /> `); - assert.dom("[data-test-product-select-selected-value]").hasText("--"); + assert.dom(".product-select-selected-value").hasText("--"); }); test("it displays the products in a dropdown list with abbreviations", async function (this: InputsProductSelectContext, assert) { From c6b689e35d70ebec68816a863f0f510e746b8714 Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Mon, 5 Jun 2023 09:36:13 -0400 Subject: [PATCH 121/128] Update factory type --- web/mirage/factories/document.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/mirage/factories/document.ts b/web/mirage/factories/document.ts index 2f23bd795..20ab984ab 100644 --- a/web/mirage/factories/document.ts +++ b/web/mirage/factories/document.ts @@ -28,11 +28,15 @@ export default Factory.extend({ docType: "RFC", modifiedAgo: 1000000000, modifiedTime: 1, - docNumber: (i: number) => `RFC-00${i}`, + docNumber() { + // @ts-ignore - Mirage types are wrong + // See discussion at https://github.com/miragejs/miragejs/pull/525 + return getTestDocNumber(this.product); + }, _snippetResult: { content: { value: "This is a test document", }, }, - owners: ["Test user"] + owners: ["Test user"], }); From 9625b2e1c66fe5eeeaf60f6d794de08e02383dfd Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 7 Jun 2023 14:14:57 -0400 Subject: [PATCH 122/128] Cleanup, documentation and UX improvements --- web/app/components/header/search.hbs | 27 ++-- web/app/components/header/search.ts | 157 +++++++++++-------- web/app/components/x/dropdown-list/index.ts | 32 ++-- web/app/components/x/dropdown-list/item.hbs | 68 ++++---- web/app/styles/components/header/search.scss | 3 +- 5 files changed, 170 insertions(+), 117 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index df6c32622..a8b9a3f1f 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -1,17 +1,13 @@ -{{! @glint-nocheck - not typesafe yet}} - -{{on-document "keydown" this.onDocumentKeydown}} +{{on-document "keydown" this.maybeFocusInput}}
    <:anchor as |dd|> <:item as |dd|> - {{#if - (or - (eq dd.value "viewAllResultsObject") - (eq dd.value "productAreaMatch") - ) - }} + + {{#if dd.attrs.renderOut}} + {{! + If the item is has the `renderOut` attribute, e.g., the "View all results" object, we render it in the popover header using the in-element helper, separating them semantically from the Best Matches. + }} {{#unless this.searchInputIsEmpty}} {{#in-element (html-element "#global-search-popover-header") @@ -68,9 +63,9 @@ class="global-search-popover-header-link" > - View all results for "{{this.query}}" + View all results for “{{this.query}}” - {{else}} + {{else if (eq dd.value "productAreaMatch")}} 1; - } - - get dropdownListStyle(): string { - return `width: calc(100% + ${ - POPOVER_BORDER_WIDTH + POPOVER_CROSS_AXIS_OFFSET - }px)`; - } - - get popoverOffset(): OffsetOptions { - return { - mainAxis: 0, - crossAxis: POPOVER_CROSS_AXIS_OFFSET, - }; + return Object.keys(this.itemsToShow).length > 1; } - get bestMatches(): SearchResultObjects { + /** + * The items to show in the dropdown. + * Always shows the "View All Results" link. + * Conditionally shows the "View all [productArea]" link + * and any document matches. + */ + get itemsToShow(): SearchResultObjects { return this._bestMatches.reduce( (acc, doc) => { acc[doc.objectID] = doc; return acc; }, { - viewAllResultsObject: {}, + viewAllResultsObject: { + renderOut: true, + }, ...(this._productAreaMatch && { - productAreaMatch: this._productAreaMatch, + productAreaMatch: { + renderOut: true, + ...this._productAreaMatch, + }, }), } as SearchResultObjects ); } - @action onInputKeydown(dd: any, e: KeyboardEvent): void { - if (e.key === "ArrowUp" || e.key === "ArrowDown") { - if (!this.query.length) { - e.preventDefault(); - return; - } - } + /** + * If the user presses enter and there's no focused item, + * then the "View All Results" link is clicked and the dropdown is closed. + */ + @action maybeSubmitForm(dd: any, e: KeyboardEvent): void { + if (e.key === "Enter") { + // Prevent the form from submitting + e.preventDefault(); - if (e.key === "Enter" && dd.focusedItemIndex === -1) { - if (!dd.selected) { - e.preventDefault(); + // if there's a search and no focused item, view all results + if (dd.focusedItemIndex === -1 && this.query.length) { this.viewAllResults(); dd.hideContent(); } } - - dd.onTriggerKeydown(dd.contentIsShown, dd.showContent, e); } + /** + * The action to run when the input is inserted. + * Registers the input so can we focus it later. + */ @action protected registerInput(element: HTMLInputElement): void { this.searchInput = element; } - @action protected onDocumentKeydown(e: KeyboardEvent): void { + /** + * The action run when on document keydown. + * If the user presses cmd+k, we focus the input. + */ + @action protected maybeFocusInput(e: KeyboardEvent): void { if (e.metaKey && (e.key === "k" || e.key === "K")) { e.preventDefault(); assert("searchInput is expected", this.searchInput); @@ -102,6 +106,10 @@ export default class HeaderSearchComponent extends Component => { let input = inputEvent.target; @@ -142,45 +167,51 @@ export default class HeaderSearchComponent extends Component values - ); - - let [productAreas, docs] = algoliaResults; - - this._bestMatches = docs - ? (docs.hits.slice(0, 5) as HermesDocument[]) - : []; - this._productAreaMatch = productAreas - ? (productAreas.hits[0] as HermesDocument) - : null; + try { + const productSearch = this.algolia.searchIndex.perform( + this.configSvc.config.algolia_docs_index_name + "_product_areas", + this.query, + { + hitsPerPage: 1, + } + ); + + const docSearch = this.algolia.search.perform(this.query, { + hitsPerPage: 5, + }); + + let algoliaResults = await Promise.all([ + productSearch, + docSearch, + ]).then((values) => values); + + let [productAreas, docs] = algoliaResults; + + this._bestMatches = docs + ? (docs.hits.slice(0, 5) as HermesDocument[]) + : []; + this._productAreaMatch = productAreas + ? (productAreas.hits[0] as HermesDocument) + : null; + } catch (e: unknown) { + console.error(e); + } } else { + this.query = ""; this._productAreaMatch = null; this.searchInputIsEmpty = true; dd.hideContent(); this._bestMatches = []; } - // Reopen the dropdown if it was closed on mousedown - if (!dd.contentIsShown) { + // Reopen the dropdown if it was closed on mousedown and there's a query + if (!dd.contentIsShown && this.query.length) { dd.showContent(); } - // Although the `dd.scheduleAssignMenuItemIDs` method runs `afterRender`, + // Although `dd.scheduleAssignMenuItemIDs` runs `afterRender`, // it doesn't provide enough time for `in-element` to update. - // Therefore, we wait for the next run loop when the DOM is updated. + // Therefore, we wait for the next run loop. next(() => { dd.resetFocusedItemIndex(); dd.scheduleAssignMenuItemIDs(); diff --git a/web/app/components/x/dropdown-list/index.ts b/web/app/components/x/dropdown-list/index.ts index 29ecd83ee..caadb4a7b 100644 --- a/web/app/components/x/dropdown-list/index.ts +++ b/web/app/components/x/dropdown-list/index.ts @@ -2,7 +2,7 @@ import { assert } from "@ember/debug"; import { action } from "@ember/object"; import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; -import { Placement } from "@floating-ui/dom"; +import { OffsetOptions, Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { restartableTask } from "ember-concurrency"; @@ -13,14 +13,17 @@ interface XDropdownListComponentSignature { Args: { items?: any; listIsOrdered?: boolean; - selected: any; + selected?: any; placement?: Placement; isSaving?: boolean; - onItemClick: (value: any) => void; + onItemClick?: (value: any) => void; + offset?: OffsetOptions; }; + // TODO: Replace using Glint's `withBoundArgs` types Blocks: { default: []; anchor: [dd: any]; + header: [dd: any]; item: [dd: any]; }; } @@ -270,9 +273,14 @@ export default class XDropdownListComponent extends Component { assert( @@ -313,9 +333,3 @@ declare module "@glint/environment-ember-loose/registry" { "X::DropdownList": typeof XDropdownListComponent; } } - -declare module "@glint/environment-ember-loose/registry" { - export default interface Registry { - "X::DropdownList": typeof XDropdownListComponent; - } -} diff --git a/web/app/components/x/dropdown-list/item.hbs b/web/app/components/x/dropdown-list/item.hbs index 7ffd90547..06c287e46 100644 --- a/web/app/components/x/dropdown-list/item.hbs +++ b/web/app/components/x/dropdown-list/item.hbs @@ -1,29 +1,43 @@ {{! @glint-nocheck: not typesafe yet }} -
  • - {{yield - (hash - Action=(component - "x/dropdown-list/action" - role=@listItemRole - isAriaSelected=this.isAriaSelected - isAriaChecked=@selected - registerElement=this.registerElement - focusMouseTarget=this.focusMouseTarget - onClick=this.onClick - ) - LinkTo=(component - "x/dropdown-list/link-to" - role=@listItemRole - isAriaSelected=this.isAriaSelected - isAriaChecked=@selected - registerElement=this.registerElement - focusMouseTarget=this.focusMouseTarget - onClick=this.onClick - ) - contentID=@contentID - value=@value - attrs=@attributes - selected=@selected - ) +{{#let (element (if @attributes.renderOut "div" "li")) as |MaybeListItem|}} + {{#maybe-in-element + (html-element ".ember-application") + (not @attributes.renderOut) + insertBefore=null }} -
  • + + {{yield + (hash + Action=(component + "x/dropdown-list/action" + role=@listItemRole + isAriaSelected=this.isAriaSelected + isAriaChecked=@selected + registerElement=this.registerElement + focusMouseTarget=this.focusMouseTarget + onClick=this.onClick + ) + LinkTo=(component + "x/dropdown-list/link-to" + role=@listItemRole + isAriaSelected=this.isAriaSelected + isAriaChecked=@selected + registerElement=this.registerElement + focusMouseTarget=this.focusMouseTarget + onClick=this.onClick + ) + contentID=@contentID + value=@value + attrs=@attributes + selected=@selected + ) + }} + + {{/maybe-in-element}} +{{/let}} diff --git a/web/app/styles/components/header/search.scss b/web/app/styles/components/header/search.scss index 6cb815002..bd0f478d0 100644 --- a/web/app/styles/components/header/search.scss +++ b/web/app/styles/components/header/search.scss @@ -1,6 +1,5 @@ .x-dropdown-list.search-popover { - // width calculated in the component class - @apply max-h-[none] min-w-[420px] max-w-[none]; + @apply w-[calc(100%+4px)] max-h-[none] min-w-[420px] max-w-[none]; &.no-best-matches { .x-dropdown-list-items { From f277a0a5db30f1fda4b952e025a16975fe7fcaba Mon Sep 17 00:00:00 2001 From: Jeff Daley Date: Wed, 7 Jun 2023 14:52:05 -0400 Subject: [PATCH 123/128] Improve shouldRenderOut documentation --- web/app/components/header/search.hbs | 4 +-- web/app/components/header/search.ts | 4 +-- web/app/components/x/dropdown-list/item.hbs | 34 ++++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/web/app/components/header/search.hbs b/web/app/components/header/search.hbs index a8b9a3f1f..fd92a2ade 100644 --- a/web/app/components/header/search.hbs +++ b/web/app/components/header/search.hbs @@ -46,9 +46,9 @@ <:item as |dd|> - {{#if dd.attrs.renderOut}} + {{#if dd.attrs.itemShouldRenderOut}} {{! - If the item is has the `renderOut` attribute, e.g., the "View all results" object, we render it in the popover header using the in-element helper, separating them semantically from the Best Matches. + We use this property to catch the "view all results" and "view all [productArea] documents" items and, for semantic purposes, render them outside of the DropdownList's primary `ul/ol` element while still retaining the keyboard navigability provided by the DropdownList component. }} {{#unless this.searchInputIsEmpty}} {{#in-element diff --git a/web/app/components/header/search.ts b/web/app/components/header/search.ts index 9f9d9e3a3..ca4664e2f 100644 --- a/web/app/components/header/search.ts +++ b/web/app/components/header/search.ts @@ -57,11 +57,11 @@ export default class HeaderSearchComponent extends Component