diff --git a/packages/calcite-components/src/components/dropdown-item/dropdown-item.e2e.ts b/packages/calcite-components/src/components/dropdown-item/dropdown-item.e2e.ts
index 3de2902a761..3c043d9b2b8 100644
--- a/packages/calcite-components/src/components/dropdown-item/dropdown-item.e2e.ts
+++ b/packages/calcite-components/src/components/dropdown-item/dropdown-item.e2e.ts
@@ -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", () => {
@@ -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(` Dropdown Item Content `);
diff --git a/packages/calcite-components/src/components/dropdown-item/dropdown-item.scss b/packages/calcite-components/src/components/dropdown-item/dropdown-item.scss
index 3aad51218ef..e409cc47d7f 100644
--- a/packages/calcite-components/src/components/dropdown-item/dropdown-item.scss
+++ b/packages/calcite-components/src/components/dropdown-item/dropdown-item.scss
@@ -219,3 +219,4 @@
}
@include base-component();
+@include disabled();
diff --git a/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx b/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx
index eb86d08cc4d..a28f837fa4d 100644
--- a/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx
+++ b/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx
@@ -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.
@@ -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;
@@ -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;
@@ -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;
diff --git a/packages/calcite-components/src/components/dropdown/dropdown.e2e.ts b/packages/calcite-components/src/components/dropdown/dropdown.e2e.ts
index 67a5f25a116..1e94f3109a4 100644
--- a/packages/calcite-components/src/components/dropdown/dropdown.e2e.ts
+++ b/packages/calcite-components/src/components/dropdown/dropdown.e2e.ts
@@ -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`
@@ -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`
+
+ Open
+
+ 1
+ 2
+ 3
+
+
+ `);
+ 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`
+
+ Open
+
+ 1
+ 1.5
+ 2
+ 2.5
+ 3
+ 4
+
+
+ `);
+ 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);
+ });
+ });
});
});
diff --git a/packages/calcite-components/src/components/dropdown/dropdown.stories.ts b/packages/calcite-components/src/components/dropdown/dropdown.stories.ts
index 1d53146e208..1f3e4aace0a 100644
--- a/packages/calcite-components/src/components/dropdown/dropdown.stories.ts
+++ b/packages/calcite-components/src/components/dropdown/dropdown.stories.ts
@@ -323,23 +323,43 @@ export const noScrollingWhenMaxItemsEqualsItems_TestOnly = (): string => html` <
`;
-export const disabled_TestOnly = (): string => html`
- Open Dropdown
-
- 1
- 2
- 3
- 4
- 5
-
-
- 6
- 7
- 8
- 9
- 10
-
-`;
+export const disabled_TestOnly = (): string => html`
+
+ Disabled dropdown
+
+ 1
+ 2
+ 3
+ 4
+ 5
+
+
+ 6
+ 7
+ 8
+ 9
+ 10
+
+
+
+
+ Disabled dropdown items
+
+ 1
+ 2
+ 3
+ 4
+ 5
+
+
+ 6
+ 7
+ 8
+ 9
+ 10
+
+
+`;
export const flipPositioning_TestOnly = (): string => html`
diff --git a/packages/calcite-components/src/components/dropdown/dropdown.tsx b/packages/calcite-components/src/components/dropdown/dropdown.tsx
index f3538a136c3..5f7d27c4fa3 100644
--- a/packages/calcite-components/src/components/dropdown/dropdown.tsx
+++ b/packages/calcite-components/src/components/dropdown/dropdown.tsx
@@ -374,10 +374,15 @@ export class Dropdown
this.closeCalciteDropdown();
}
+ private getTraversableItems(): HTMLCalciteDropdownItemElement[] {
+ return this.items.filter((item) => !item.disabled && !item.hidden);
+ }
+
@Listen("calciteInternalDropdownItemKeyEvent")
calciteInternalDropdownItemKeyEvent(event: CustomEvent): void {
const { keyboardEvent } = event.detail;
const target = keyboardEvent.target as HTMLCalciteDropdownItemElement;
+ const traversableItems = this.getTraversableItems();
switch (keyboardEvent.key) {
case "Tab":
@@ -385,16 +390,16 @@ export class Dropdown
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;
}
@@ -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 {