Skip to content

Commit

Permalink
Add BadgeDropdownList component (#185)
Browse files Browse the repository at this point in the history
* BadgeDropdownList component

* BadgeDropdownList component

* Add custom-item test

* Remove unnecessary comment

* Resolve merge conflict

* Remove product specificity
  • Loading branch information
jeffdaley authored May 31, 2023
1 parent 980d1f8 commit ea2dbc1
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 9 deletions.
51 changes: 51 additions & 0 deletions web/app/components/inputs/badge-dropdown-list.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{{! @glint-nocheck - not typesafe yet }}
<X::DropdownList
@items={{@items}}
@listIsOrdered={{@listIsOrdered}}
@onItemClick={{@onItemClick}}
@selected={{@selected}}
...attributes
>
<:anchor as |dd|>
<div class="relative w-full">
{{#if @isSaving}}
<div class="absolute right-0 top-1/2 -translate-y-1/2">
<FlightIcon
data-test-badge-dropdown-list-saving-icon
@name="loading"
/>
</div>
{{/if}}
<dd.ToggleAction
data-test-badge-dropdown-trigger
class="relative {{if @isSaving 'opacity-50'}}"
>
<Hds::Badge
data-test-badge-dropdown-list-icon
data-test-icon={{if @selected (get-product-id @selected)}}
@text={{or @selected "--"}}
@icon={{@icon}}
class="hds-badge-dropdown"
/>
<FlightIcon
data-test-badge-dropdown-list-chevron-icon
data-test-chevron-position={{if dd.contentIsShown "up" "down"}}
@name={{if dd.contentIsShown "chevron-up" "chevron-down"}}
class="dropdown-caret"
/>
</dd.ToggleAction>
</div>
</:anchor>
<:item as |dd|>
{{#if (has-block "item")}}
{{yield dd to="item"}}
{{else}}
<dd.Action data-test-badge-dropdown-list-default-action>
<X::DropdownList::CheckableItem
@selected={{dd.selected}}
@value={{dd.value}}
/>
</dd.Action>
{{/if}}
</:item>
</X::DropdownList>
27 changes: 27 additions & 0 deletions web/app/components/inputs/badge-dropdown-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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;
icon: string;
};
Blocks: {
default: [];
item: [dd: any];
};
}

export default class InputsBadgeDropdownListComponent extends Component<InputsBadgeDropdownListComponentSignature> {}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
"Inputs::BadgeDropdownList": typeof InputsBadgeDropdownListComponent;
}
}
2 changes: 2 additions & 0 deletions web/app/components/x/dropdown-list/checkable-item.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{{! @glint-nocheck - not typesafe yet}}
<FlightIcon
data-test-x-dropdown-list-checkable-item-check
data-test-is-checked={{@selected}}
@name="check"
class="check {{if @selected 'visible' 'invisible'}}"
/>
Expand Down
6 changes: 6 additions & 0 deletions web/app/components/x/dropdown-list/checkable-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ interface XDropdownListCheckableItemComponentSignature {
}

export default class XDropdownListCheckableItemComponent extends Component<XDropdownListCheckableItemComponentSignature> {}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
"X::DropdownList::CheckableItem": typeof XDropdownListCheckableItemComponent;
}
}
23 changes: 17 additions & 6 deletions web/app/components/x/dropdown-list/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -85,12 +86,22 @@ export default class XDropdownListItemComponent extends Component<XDropdownListI
}

/**
* Closes the dropdown on the next run loop.
* Done so we don't interfere with Ember's <LinkTo> handling.
* In production, close the dropdown on the next run loop
* so that we don't interfere with Ember's <LinkTo> 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();
});
}
}

/**
Expand Down
16 changes: 13 additions & 3 deletions web/app/styles/components/x/dropdown/list-item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,27 @@
}
}

&: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;
}
}

&.sort-icon {
@apply ml-3 mr-4;
}
}

.x-dropdown-list-item-value {
Expand Down
8 changes: 8 additions & 0 deletions web/app/styles/hds-overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@
}
}
}

.hds-badge-dropdown {
@apply pr-6;

+ .dropdown-caret {
@apply absolute right-1.5 top-1/2 -translate-y-1/2;
}
}
130 changes: 130 additions & 0 deletions web/tests/integration/components/inputs/badge-dropdown-list-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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";
import getProductId from "hermes/utils/get-product-id";

interface BadgeDropdownListTestContext extends MirageTestContext {
items: any;
selected?: any;
listIsOrdered?: boolean;
isSaving?: boolean;
onItemClick: ((e: Event) => void) | ((selected: string) => void);
placement?: Placement;
icon: string;
updateIcon: () => void;
}

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);

hooks.beforeEach(function (this: BadgeDropdownListTestContext) {
this.items = { Waypoint: {}, Labs: {}, Boundary: {} };
this.selected = Object.keys(this.items)[1];

const updateIcon = () => {
let icon = "folder";
if (this.selected && getProductId(this.selected)) {
icon = getProductId(this.selected) as string;
}
this.set("icon", icon);
};

updateIcon();

this.onItemClick = (selected: string) => {
this.set("selected", selected);
updateIcon();
};
});

test("it functions as expected (default checkable item)", async function (this: BadgeDropdownListTestContext, assert) {
await render<BadgeDropdownListTestContext>(hbs`
{{! @glint-ignore: not typed yet }}
<Inputs::BadgeDropdownList
@items={{this.items}}
@selected={{this.selected}}
@onItemClick={{this.onItemClick}}
@icon={{this.icon}}
/>
`);

const iconSelector = "[data-test-badge-dropdown-list-icon] .flight-icon";
const chevronSelector = "[data-test-badge-dropdown-list-chevron-icon]";

assert.dom(iconSelector).hasAttribute("data-test-icon", "folder");
assert.dom(TRIGGER_SELECTOR).hasText("Labs");
assert
.dom(chevronSelector)
.hasAttribute("data-test-chevron-position", "down");

await click(TRIGGER_SELECTOR);

let listItemsText = findAll(DEFAULT_ACTION_SELECTOR).map((el) =>
el.textContent?.trim()
);

assert.deepEqual(
listItemsText,
["Waypoint", "Labs", "Boundary"],
"correct list items are rendered"
);

assert
.dom(
`${ITEM_SELECTOR}:nth-child(2) [data-test-x-dropdown-list-checkable-item-check]`
)
.hasAttribute("data-test-is-checked");

await click(DEFAULT_ACTION_SELECTOR);

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<BadgeDropdownListTestContext>(hbs`
{{! @glint-ignore: not typed yet }}
<Inputs::BadgeDropdownList
@items={{this.items}}
@selected={{this.selected}}
@onItemClick={{this.onItemClick}}
@icon={{this.icon}}
>
<:item as |dd|>
<dd.Action>
{{dd.value}}
{{#if dd.selected}}
<span>(selected)</span>
{{/if}}
</dd.Action>
</:item>
</Inputs::BadgeDropdownList>
`);

await click(TRIGGER_SELECTOR);

assert.dom(ITEM_SELECTOR).hasText("Waypoint");

assert
.dom(DEFAULT_ACTION_SELECTOR)
.doesNotExist("default action is not rendered");

assert.dom(`${ITEM_SELECTOR}:nth-child(2)`).hasText("Labs (selected)");
});
}
);

0 comments on commit ea2dbc1

Please sign in to comment.