Skip to content

Commit 4c311c6

Browse files
authored
feat(dropdown-item): add disabled support (#8312)
**Related Issue:** #6667 ## Summary This allows disabling dropdown items.
1 parent 211aaf0 commit 4c311c6

File tree

6 files changed

+189
-34
lines changed

6 files changed

+189
-34
lines changed

packages/calcite-components/src/components/dropdown-item/dropdown-item.e2e.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { newE2EPage } from "@stencil/core/testing";
2-
import { focusable, renders, hidden } from "../../tests/commonTests";
2+
import { focusable, renders, hidden, disabled } from "../../tests/commonTests";
33

44
describe("calcite-dropdown-item", () => {
55
describe("renders", () => {
@@ -14,6 +14,10 @@ describe("calcite-dropdown-item", () => {
1414
focusable(`calcite-dropdown-item`);
1515
});
1616

17+
describe("disabled", () => {
18+
disabled(`calcite-dropdown-item`);
19+
});
20+
1721
it("should emit calciteDropdownItemSelect", async () => {
1822
const page = await newE2EPage();
1923
await page.setContent(`<calcite-dropdown-item id="item-1"> Dropdown Item Content </calcite-dropdown-item>`);

packages/calcite-components/src/components/dropdown-item/dropdown-item.scss

+1
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,4 @@
219219
}
220220

221221
@include base-component();
222+
@include disabled();

packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx

+20-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
setUpLoadableComponent,
2323
} from "../../utils/loadable";
2424
import { getIconScale } from "../../utils/component";
25+
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
2526

2627
/**
2728
* @slot - A slot for adding text.
@@ -31,15 +32,24 @@ import { getIconScale } from "../../utils/component";
3132
styleUrl: "dropdown-item.scss",
3233
shadow: true,
3334
})
34-
export class DropdownItem implements LoadableComponent {
35+
export class DropdownItem implements InteractiveComponent, LoadableComponent {
3536
//--------------------------------------------------------------------------
3637
//
3738
// Public Properties
3839
//
3940
//--------------------------------------------------------------------------
4041

41-
/** When `true`, the component is selected. */
42-
@Prop({ reflect: true, mutable: true }) selected = false;
42+
/**
43+
* When `true`, interaction is prevented and the component is displayed with lower opacity.
44+
*/
45+
@Prop({ reflect: true }) disabled = false;
46+
47+
/**
48+
* Specifies the URL of the linked resource, which can be set as an absolute or relative path.
49+
*
50+
* Determines if the component will render as an anchor.
51+
*/
52+
@Prop({ reflect: true }) href: string;
4353

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

53-
/**
54-
* Specifies the URL of the linked resource, which can be set as an absolute or relative path.
55-
*
56-
* Determines if the component will render as an anchor.
57-
*/
58-
@Prop({ reflect: true }) href: string;
59-
6063
/** Accessible name for the component. */
6164
@Prop() label: string;
6265

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

69+
/** When `true`, the component is selected. */
70+
@Prop({ reflect: true, mutable: true }) selected = false;
71+
6672
/** Specifies the frame or window to open the linked document. */
6773
@Prop({ reflect: true }) target: string;
6874

@@ -136,6 +142,10 @@ export class DropdownItem implements LoadableComponent {
136142
this.initialize();
137143
}
138144

145+
componentDidRender(): void {
146+
updateHostInteraction(this, "managed");
147+
}
148+
139149
render(): VNode {
140150
const { href, selectionMode, label, iconFlipRtl, scale } = this;
141151

packages/calcite-components/src/components/dropdown/dropdown.e2e.ts

+114-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
reflects,
1313
renders,
1414
} from "../../tests/commonTests";
15-
import { GlobalTestProps, getFocusedElementProp } from "../../tests/utils";
15+
import { GlobalTestProps, getFocusedElementProp, isElementFocused, skipAnimations } from "../../tests/utils";
1616

1717
describe("calcite-dropdown", () => {
1818
const simpleDropdownHTML = html`
@@ -1232,5 +1232,118 @@ describe("calcite-dropdown", () => {
12321232
}
12331233
);
12341234
});
1235+
1236+
describe("keyboard navigation", () => {
1237+
it("supports navigating through items with arrow keys", async () => {
1238+
const page = await newE2EPage();
1239+
await page.setContent(html`
1240+
<calcite-dropdown>
1241+
<calcite-button slot="trigger">Open</calcite-button>
1242+
<calcite-dropdown-group selection-mode="single">
1243+
<calcite-dropdown-item id="item-1" selected>1</calcite-dropdown-item>
1244+
<calcite-dropdown-item id="item-2">2</calcite-dropdown-item>
1245+
<calcite-dropdown-item id="item-3">3</calcite-dropdown-item>
1246+
</calcite-dropdown-group>
1247+
</calcite-dropdown>
1248+
`);
1249+
await skipAnimations(page);
1250+
1251+
const dropdown = await page.find("calcite-dropdown");
1252+
await dropdown.callMethod("setFocus");
1253+
await page.waitForChanges();
1254+
1255+
await page.keyboard.press("Enter");
1256+
await page.waitForChanges();
1257+
1258+
expect(await isElementFocused(page, "#item-1")).toBe(true);
1259+
1260+
await page.keyboard.press("ArrowDown");
1261+
await page.waitForChanges();
1262+
1263+
expect(await isElementFocused(page, "#item-2")).toBe(true);
1264+
1265+
await page.keyboard.press("ArrowDown");
1266+
await page.waitForChanges();
1267+
1268+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1269+
1270+
await page.keyboard.press("ArrowDown");
1271+
await page.waitForChanges();
1272+
1273+
expect(await isElementFocused(page, "#item-1")).toBe(true);
1274+
1275+
await page.keyboard.press("ArrowUp");
1276+
await page.waitForChanges();
1277+
1278+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1279+
1280+
await page.keyboard.press("ArrowUp");
1281+
await page.waitForChanges();
1282+
1283+
expect(await isElementFocused(page, "#item-2")).toBe(true);
1284+
1285+
await page.keyboard.press("ArrowUp");
1286+
await page.waitForChanges();
1287+
1288+
expect(await isElementFocused(page, "#item-1")).toBe(true);
1289+
1290+
await page.keyboard.press("ArrowUp");
1291+
await page.waitForChanges();
1292+
1293+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1294+
});
1295+
1296+
it("skips disabled and hidden items when navigating with arrow keys", async () => {
1297+
const page = await newE2EPage();
1298+
await page.setContent(html`
1299+
<calcite-dropdown>
1300+
<calcite-button slot="trigger">Open</calcite-button>
1301+
<calcite-dropdown-group selection-mode="single">
1302+
<calcite-dropdown-item id="item-1" disabled>1</calcite-dropdown-item>
1303+
<calcite-dropdown-item id="item-1.5" disabled>1.5</calcite-dropdown-item>
1304+
<calcite-dropdown-item id="item-2" selected>2</calcite-dropdown-item>
1305+
<calcite-dropdown-item id="item-2.5" hidden>2.5</calcite-dropdown-item>
1306+
<calcite-dropdown-item id="item-3">3</calcite-dropdown-item>
1307+
<calcite-dropdown-item id="item-4" hidden>4</calcite-dropdown-item>
1308+
</calcite-dropdown-group>
1309+
</calcite-dropdown>
1310+
`);
1311+
await skipAnimations(page);
1312+
1313+
const dropdown = await page.find("calcite-dropdown");
1314+
await dropdown.callMethod("setFocus");
1315+
await page.waitForChanges();
1316+
1317+
await page.keyboard.press("Enter");
1318+
await page.waitForChanges();
1319+
1320+
expect(await isElementFocused(page, "#item-2")).toBe(true);
1321+
1322+
await page.keyboard.press("ArrowDown");
1323+
await page.waitForChanges();
1324+
1325+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1326+
1327+
await page.keyboard.press("ArrowDown");
1328+
await page.waitForChanges();
1329+
1330+
expect(await isElementFocused(page, "#item-2")).toBe(true);
1331+
1332+
await page.keyboard.press("ArrowUp");
1333+
await page.waitForChanges();
1334+
1335+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1336+
1337+
await page.keyboard.press("ArrowUp");
1338+
await page.waitForChanges();
1339+
1340+
expect(await isElementFocused(page, "#item-2")).toBe(true);
1341+
1342+
await page.keyboard.press("ArrowUp");
1343+
await page.waitForChanges();
1344+
1345+
expect(await isElementFocused(page, "#item-3")).toBe(true);
1346+
});
1347+
});
12351348
});
12361349
});

packages/calcite-components/src/components/dropdown/dropdown.stories.ts

+37-17
Original file line numberDiff line numberDiff line change
@@ -323,23 +323,43 @@ export const noScrollingWhenMaxItemsEqualsItems_TestOnly = (): string => html` <
323323
</calcite-dropdown-group>
324324
</calcite-dropdown>`;
325325

326-
export const disabled_TestOnly = (): string => html` <calcite-dropdown disabled>
327-
<calcite-button slot="trigger">Open Dropdown</calcite-button>
328-
<calcite-dropdown-group group-title="First group">
329-
<calcite-dropdown-item>1</calcite-dropdown-item>
330-
<calcite-dropdown-item>2</calcite-dropdown-item>
331-
<calcite-dropdown-item>3</calcite-dropdown-item>
332-
<calcite-dropdown-item>4</calcite-dropdown-item>
333-
<calcite-dropdown-item>5</calcite-dropdown-item>
334-
</calcite-dropdown-group>
335-
<calcite-dropdown-group group-title="Second group">
336-
<calcite-dropdown-item>6</calcite-dropdown-item>
337-
<calcite-dropdown-item>7</calcite-dropdown-item>
338-
<calcite-dropdown-item>8</calcite-dropdown-item>
339-
<calcite-dropdown-item>9</calcite-dropdown-item>
340-
<calcite-dropdown-item>10</calcite-dropdown-item>
341-
</calcite-dropdown-group>
342-
</calcite-dropdown>`;
326+
export const disabled_TestOnly = (): string => html`
327+
<calcite-dropdown disabled>
328+
<calcite-button slot="trigger">Disabled dropdown</calcite-button>
329+
<calcite-dropdown-group group-title="First group">
330+
<calcite-dropdown-item>1</calcite-dropdown-item>
331+
<calcite-dropdown-item>2</calcite-dropdown-item>
332+
<calcite-dropdown-item>3</calcite-dropdown-item>
333+
<calcite-dropdown-item>4</calcite-dropdown-item>
334+
<calcite-dropdown-item>5</calcite-dropdown-item>
335+
</calcite-dropdown-group>
336+
<calcite-dropdown-group group-title="Second group">
337+
<calcite-dropdown-item>6</calcite-dropdown-item>
338+
<calcite-dropdown-item>7</calcite-dropdown-item>
339+
<calcite-dropdown-item>8</calcite-dropdown-item>
340+
<calcite-dropdown-item>9</calcite-dropdown-item>
341+
<calcite-dropdown-item>10</calcite-dropdown-item>
342+
</calcite-dropdown-group>
343+
</calcite-dropdown>
344+
345+
<calcite-dropdown open>
346+
<calcite-button slot="trigger">Disabled dropdown items</calcite-button>
347+
<calcite-dropdown-group group-title="First group">
348+
<calcite-dropdown-item>1</calcite-dropdown-item>
349+
<calcite-dropdown-item disabled>2</calcite-dropdown-item>
350+
<calcite-dropdown-item disabled>3</calcite-dropdown-item>
351+
<calcite-dropdown-item disabled>4</calcite-dropdown-item>
352+
<calcite-dropdown-item>5</calcite-dropdown-item>
353+
</calcite-dropdown-group>
354+
<calcite-dropdown-group group-title="Second group">
355+
<calcite-dropdown-item>6</calcite-dropdown-item>
356+
<calcite-dropdown-item>7</calcite-dropdown-item>
357+
<calcite-dropdown-item>8</calcite-dropdown-item>
358+
<calcite-dropdown-item>9</calcite-dropdown-item>
359+
<calcite-dropdown-item>10</calcite-dropdown-item>
360+
</calcite-dropdown-group>
361+
</calcite-dropdown>
362+
`;
343363

344364
export const flipPositioning_TestOnly = (): string => html`
345365
<div style="margin:10px;">

packages/calcite-components/src/components/dropdown/dropdown.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -374,27 +374,32 @@ export class Dropdown
374374
this.closeCalciteDropdown();
375375
}
376376

377+
private getTraversableItems(): HTMLCalciteDropdownItemElement[] {
378+
return this.items.filter((item) => !item.disabled && !item.hidden);
379+
}
380+
377381
@Listen("calciteInternalDropdownItemKeyEvent")
378382
calciteInternalDropdownItemKeyEvent(event: CustomEvent<ItemKeyboardEvent>): void {
379383
const { keyboardEvent } = event.detail;
380384
const target = keyboardEvent.target as HTMLCalciteDropdownItemElement;
385+
const traversableItems = this.getTraversableItems();
381386

382387
switch (keyboardEvent.key) {
383388
case "Tab":
384389
this.open = false;
385390
this.updateTabIndexOfItems(target);
386391
break;
387392
case "ArrowDown":
388-
focusElementInGroup(this.items, target, "next");
393+
focusElementInGroup(traversableItems, target, "next");
389394
break;
390395
case "ArrowUp":
391-
focusElementInGroup(this.items, target, "previous");
396+
focusElementInGroup(traversableItems, target, "previous");
392397
break;
393398
case "Home":
394-
focusElementInGroup(this.items, target, "first");
399+
focusElementInGroup(traversableItems, target, "first");
395400
break;
396401
case "End":
397-
focusElementInGroup(this.items, target, "last");
402+
focusElementInGroup(traversableItems, target, "last");
398403
break;
399404
}
400405

@@ -645,7 +650,9 @@ export class Dropdown
645650
}
646651

647652
private focusOnFirstActiveOrFirstItem = (): void => {
648-
this.getFocusableElement(this.items.find((item) => item.selected) || this.items[0]);
653+
this.getFocusableElement(
654+
this.getTraversableItems().find((item) => item.selected) || this.items[0]
655+
);
649656
};
650657

651658
private getFocusableElement(item): void {

0 commit comments

Comments
 (0)