Skip to content

Commit

Permalink
feat(dropdown-item): add disabled support (#8312)
Browse files Browse the repository at this point in the history
**Related Issue:** #6667

## Summary

This allows disabling dropdown items.
  • Loading branch information
jcfranco authored Dec 4, 2023
1 parent 211aaf0 commit 4c311c6
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { newE2EPage } from "@stencil/core/testing";
import { focusable, renders, hidden } from "../../tests/commonTests";
import { focusable, renders, hidden, disabled } from "../../tests/commonTests";

describe("calcite-dropdown-item", () => {
describe("renders", () => {
Expand All @@ -14,6 +14,10 @@ describe("calcite-dropdown-item", () => {
focusable(`calcite-dropdown-item`);
});

describe("disabled", () => {
disabled(`calcite-dropdown-item`);
});

it("should emit calciteDropdownItemSelect", async () => {
const page = await newE2EPage();
await page.setContent(`<calcite-dropdown-item id="item-1"> Dropdown Item Content </calcite-dropdown-item>`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,4 @@
}

@include base-component();
@include disabled();
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
setUpLoadableComponent,
} from "../../utils/loadable";
import { getIconScale } from "../../utils/component";
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";

/**
* @slot - A slot for adding text.
Expand All @@ -31,15 +32,24 @@ import { getIconScale } from "../../utils/component";
styleUrl: "dropdown-item.scss",
shadow: true,
})
export class DropdownItem implements LoadableComponent {
export class DropdownItem implements InteractiveComponent, LoadableComponent {
//--------------------------------------------------------------------------
//
// Public Properties
//
//--------------------------------------------------------------------------

/** When `true`, the component is selected. */
@Prop({ reflect: true, mutable: true }) selected = false;
/**
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
@Prop({ reflect: true }) disabled = false;

/**
* Specifies the URL of the linked resource, which can be set as an absolute or relative path.
*
* Determines if the component will render as an anchor.
*/
@Prop({ reflect: true }) href: string;

/** Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). */
@Prop({ reflect: true }) iconFlipRtl: FlipContext;
Expand All @@ -50,19 +60,15 @@ export class DropdownItem implements LoadableComponent {
/** Specifies an icon to display at the end of the component. */
@Prop({ reflect: true }) iconEnd: string;

/**
* Specifies the URL of the linked resource, which can be set as an absolute or relative path.
*
* Determines if the component will render as an anchor.
*/
@Prop({ reflect: true }) href: string;

/** Accessible name for the component. */
@Prop() label: string;

/** Specifies the relationship to the linked document defined in `href`. */
@Prop({ reflect: true }) rel: string;

/** When `true`, the component is selected. */
@Prop({ reflect: true, mutable: true }) selected = false;

/** Specifies the frame or window to open the linked document. */
@Prop({ reflect: true }) target: string;

Expand Down Expand Up @@ -136,6 +142,10 @@ export class DropdownItem implements LoadableComponent {
this.initialize();
}

componentDidRender(): void {
updateHostInteraction(this, "managed");
}

render(): VNode {
const { href, selectionMode, label, iconFlipRtl, scale } = this;

Expand Down
115 changes: 114 additions & 1 deletion packages/calcite-components/src/components/dropdown/dropdown.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
reflects,
renders,
} from "../../tests/commonTests";
import { GlobalTestProps, getFocusedElementProp } from "../../tests/utils";
import { GlobalTestProps, getFocusedElementProp, isElementFocused, skipAnimations } from "../../tests/utils";

describe("calcite-dropdown", () => {
const simpleDropdownHTML = html`
Expand Down Expand Up @@ -1232,5 +1232,118 @@ describe("calcite-dropdown", () => {
}
);
});

describe("keyboard navigation", () => {
it("supports navigating through items with arrow keys", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-dropdown>
<calcite-button slot="trigger">Open</calcite-button>
<calcite-dropdown-group selection-mode="single">
<calcite-dropdown-item id="item-1" selected>1</calcite-dropdown-item>
<calcite-dropdown-item id="item-2">2</calcite-dropdown-item>
<calcite-dropdown-item id="item-3">3</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
`);
await skipAnimations(page);

const dropdown = await page.find("calcite-dropdown");
await dropdown.callMethod("setFocus");
await page.waitForChanges();

await page.keyboard.press("Enter");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-1")).toBe(true);

await page.keyboard.press("ArrowDown");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-2")).toBe(true);

await page.keyboard.press("ArrowDown");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);

await page.keyboard.press("ArrowDown");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-1")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-2")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-1")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);
});

it("skips disabled and hidden items when navigating with arrow keys", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-dropdown>
<calcite-button slot="trigger">Open</calcite-button>
<calcite-dropdown-group selection-mode="single">
<calcite-dropdown-item id="item-1" disabled>1</calcite-dropdown-item>
<calcite-dropdown-item id="item-1.5" disabled>1.5</calcite-dropdown-item>
<calcite-dropdown-item id="item-2" selected>2</calcite-dropdown-item>
<calcite-dropdown-item id="item-2.5" hidden>2.5</calcite-dropdown-item>
<calcite-dropdown-item id="item-3">3</calcite-dropdown-item>
<calcite-dropdown-item id="item-4" hidden>4</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
`);
await skipAnimations(page);

const dropdown = await page.find("calcite-dropdown");
await dropdown.callMethod("setFocus");
await page.waitForChanges();

await page.keyboard.press("Enter");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-2")).toBe(true);

await page.keyboard.press("ArrowDown");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);

await page.keyboard.press("ArrowDown");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-2")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-2")).toBe(true);

await page.keyboard.press("ArrowUp");
await page.waitForChanges();

expect(await isElementFocused(page, "#item-3")).toBe(true);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -323,23 +323,43 @@ export const noScrollingWhenMaxItemsEqualsItems_TestOnly = (): string => html` <
</calcite-dropdown-group>
</calcite-dropdown>`;

export const disabled_TestOnly = (): string => html` <calcite-dropdown disabled>
<calcite-button slot="trigger">Open Dropdown</calcite-button>
<calcite-dropdown-group group-title="First group">
<calcite-dropdown-item>1</calcite-dropdown-item>
<calcite-dropdown-item>2</calcite-dropdown-item>
<calcite-dropdown-item>3</calcite-dropdown-item>
<calcite-dropdown-item>4</calcite-dropdown-item>
<calcite-dropdown-item>5</calcite-dropdown-item>
</calcite-dropdown-group>
<calcite-dropdown-group group-title="Second group">
<calcite-dropdown-item>6</calcite-dropdown-item>
<calcite-dropdown-item>7</calcite-dropdown-item>
<calcite-dropdown-item>8</calcite-dropdown-item>
<calcite-dropdown-item>9</calcite-dropdown-item>
<calcite-dropdown-item>10</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>`;
export const disabled_TestOnly = (): string => html`
<calcite-dropdown disabled>
<calcite-button slot="trigger">Disabled dropdown</calcite-button>
<calcite-dropdown-group group-title="First group">
<calcite-dropdown-item>1</calcite-dropdown-item>
<calcite-dropdown-item>2</calcite-dropdown-item>
<calcite-dropdown-item>3</calcite-dropdown-item>
<calcite-dropdown-item>4</calcite-dropdown-item>
<calcite-dropdown-item>5</calcite-dropdown-item>
</calcite-dropdown-group>
<calcite-dropdown-group group-title="Second group">
<calcite-dropdown-item>6</calcite-dropdown-item>
<calcite-dropdown-item>7</calcite-dropdown-item>
<calcite-dropdown-item>8</calcite-dropdown-item>
<calcite-dropdown-item>9</calcite-dropdown-item>
<calcite-dropdown-item>10</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
<calcite-dropdown open>
<calcite-button slot="trigger">Disabled dropdown items</calcite-button>
<calcite-dropdown-group group-title="First group">
<calcite-dropdown-item>1</calcite-dropdown-item>
<calcite-dropdown-item disabled>2</calcite-dropdown-item>
<calcite-dropdown-item disabled>3</calcite-dropdown-item>
<calcite-dropdown-item disabled>4</calcite-dropdown-item>
<calcite-dropdown-item>5</calcite-dropdown-item>
</calcite-dropdown-group>
<calcite-dropdown-group group-title="Second group">
<calcite-dropdown-item>6</calcite-dropdown-item>
<calcite-dropdown-item>7</calcite-dropdown-item>
<calcite-dropdown-item>8</calcite-dropdown-item>
<calcite-dropdown-item>9</calcite-dropdown-item>
<calcite-dropdown-item>10</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
`;

export const flipPositioning_TestOnly = (): string => html`
<div style="margin:10px;">
Expand Down
17 changes: 12 additions & 5 deletions packages/calcite-components/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,27 +374,32 @@ export class Dropdown
this.closeCalciteDropdown();
}

private getTraversableItems(): HTMLCalciteDropdownItemElement[] {
return this.items.filter((item) => !item.disabled && !item.hidden);
}

@Listen("calciteInternalDropdownItemKeyEvent")
calciteInternalDropdownItemKeyEvent(event: CustomEvent<ItemKeyboardEvent>): void {
const { keyboardEvent } = event.detail;
const target = keyboardEvent.target as HTMLCalciteDropdownItemElement;
const traversableItems = this.getTraversableItems();

switch (keyboardEvent.key) {
case "Tab":
this.open = false;
this.updateTabIndexOfItems(target);
break;
case "ArrowDown":
focusElementInGroup(this.items, target, "next");
focusElementInGroup(traversableItems, target, "next");
break;
case "ArrowUp":
focusElementInGroup(this.items, target, "previous");
focusElementInGroup(traversableItems, target, "previous");
break;
case "Home":
focusElementInGroup(this.items, target, "first");
focusElementInGroup(traversableItems, target, "first");
break;
case "End":
focusElementInGroup(this.items, target, "last");
focusElementInGroup(traversableItems, target, "last");
break;
}

Expand Down Expand Up @@ -645,7 +650,9 @@ export class Dropdown
}

private focusOnFirstActiveOrFirstItem = (): void => {
this.getFocusableElement(this.items.find((item) => item.selected) || this.items[0]);
this.getFocusableElement(
this.getTraversableItems().find((item) => item.selected) || this.items[0]
);
};

private getFocusableElement(item): void {
Expand Down

0 comments on commit 4c311c6

Please sign in to comment.