Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dropdown-item): add disabled support #8312

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`);
jcfranco marked this conversation as resolved.
Show resolved Hide resolved
});

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
Loading