diff --git a/src/components/action-group/action-group.e2e.ts b/src/components/action-group/action-group.e2e.ts
index 6cc45bfea59..84176f49f19 100755
--- a/src/components/action-group/action-group.e2e.ts
+++ b/src/components/action-group/action-group.e2e.ts
@@ -1,31 +1,26 @@
-import { accessible, hidden, renders, slots, t9n } from "../../tests/commonTests";
import { newE2EPage } from "@stencil/core/testing";
+import { accessible, focusable, hidden, renders, slots, t9n } from "../../tests/commonTests";
import { SLOTS } from "./resources";
+const actionGroupHTML = `
+
+
+ `;
+
describe("calcite-action-group", () => {
it("renders", async () => renders("calcite-action-group", { display: "flex" }));
+ it("focusable", async () => focusable(actionGroupHTML, { shadowFocusTargetSelector: "calcite-action" }));
+
it("honors hidden attribute", async () => hidden("calcite-action-group"));
- it("should be accessible", async () =>
- accessible(`
-
-
-
- `));
+ it("should be accessible", async () => accessible(actionGroupHTML));
it("has slots", () => slots("calcite-action-group", SLOTS));
it("should honor scale of expand icon", async () => {
- const page = await newE2EPage({
- html: `
-
-
- `
- });
-
+ const page = await newE2EPage({ html: actionGroupHTML });
const menu = await page.find(`calcite-action-group >>> calcite-action-menu`);
-
expect(await menu.getProperty("scale")).toBe("l");
});
diff --git a/src/components/action-group/action-group.tsx b/src/components/action-group/action-group.tsx
index da3972a3545..c97b98d5382 100755
--- a/src/components/action-group/action-group.tsx
+++ b/src/components/action-group/action-group.tsx
@@ -1,5 +1,4 @@
-import { Component, Element, h, Prop, Watch } from "@stencil/core";
-import { Fragment, State, VNode } from "@stencil/core/internal";
+import { Component, Element, Fragment, h, Method, Prop, State, VNode, Watch } from "@stencil/core";
import { CalciteActionMenuCustomEvent } from "../../components";
import {
ConditionalSlotComponent,
@@ -7,6 +6,12 @@ import {
disconnectConditionalSlotComponent
} from "../../utils/conditionalSlot";
import { getSlotted } from "../../utils/dom";
+import {
+ componentLoaded,
+ LoadableComponent,
+ setComponentLoaded,
+ setUpLoadableComponent
+} from "../../utils/loadable";
import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale";
import {
connectMessages,
@@ -33,7 +38,9 @@ import { ICONS, SLOTS } from "./resources";
},
assetsDirs: ["assets"]
})
-export class ActionGroup implements ConditionalSlotComponent, LocalizedComponent, T9nComponent {
+export class ActionGroup
+ implements ConditionalSlotComponent, LoadableComponent, LocalizedComponent, T9nComponent
+{
// --------------------------------------------------------------------------
//
// Properties
@@ -103,6 +110,18 @@ export class ActionGroup implements ConditionalSlotComponent, LocalizedComponent
@State() defaultMessages: ActionGroupMessages;
+ //--------------------------------------------------------------------------
+ //
+ // Public Methods
+ //
+ //--------------------------------------------------------------------------
+
+ /** Sets focus on the component's first focusable element. */
+ @Method()
+ async setFocus(): Promise {
+ await componentLoaded(this);
+ this.el.focus();
+ }
// --------------------------------------------------------------------------
//
// Lifecycle
@@ -122,9 +141,14 @@ export class ActionGroup implements ConditionalSlotComponent, LocalizedComponent
}
async componentWillLoad(): Promise {
+ setUpLoadableComponent(this);
await setUpMessages(this);
}
+ componentDidLoad(): void {
+ setComponentLoaded(this);
+ }
+
// --------------------------------------------------------------------------
//
// Component Methods
diff --git a/src/components/date-picker/date-picker.e2e.ts b/src/components/date-picker/date-picker.e2e.ts
index 10473c3044d..7414b3062ac 100644
--- a/src/components/date-picker/date-picker.e2e.ts
+++ b/src/components/date-picker/date-picker.e2e.ts
@@ -1,8 +1,7 @@
import { E2EPage, newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
-import { defaults, hidden, renders, t9n } from "../../tests/commonTests";
+import { defaults, focusable, hidden, renders, t9n } from "../../tests/commonTests";
import { skipAnimations } from "../../tests/utils";
-import { dateFromISO } from "../../utils/date";
import { formatTimePart } from "../../utils/time";
describe("calcite-date-picker", () => {
@@ -18,6 +17,11 @@ describe("calcite-date-picker", () => {
}
]));
+ it("focusable", async () =>
+ focusable("", {
+ shadowFocusTargetSelector: "calcite-date-picker-month-header"
+ }));
+
const animationDurationInMs = 200;
it("fires a calciteDatePickerChange event when changing year in header", async () => {
@@ -209,7 +213,8 @@ describe("calcite-date-picker", () => {
await page.setContent("");
const date = await page.find("calcite-date-picker");
const changedEvent = await page.spyOnEvent("calciteDatePickerChange");
- await date.setProperty("value", "2001-10-28");
+ date.setProperty("value", "2001-10-28");
+ await page.waitForChanges();
expect(changedEvent).toHaveReceivedEventTimes(0);
});
diff --git a/src/components/date-picker/date-picker.tsx b/src/components/date-picker/date-picker.tsx
index 98d556e913c..98af0f05ff0 100644
--- a/src/components/date-picker/date-picker.tsx
+++ b/src/components/date-picker/date-picker.tsx
@@ -6,6 +6,7 @@ import {
EventEmitter,
h,
Host,
+ Method,
Prop,
State,
VNode,
@@ -19,6 +20,12 @@ import {
HoverRange,
setEndOfDay
} from "../../utils/date";
+import {
+ componentLoaded,
+ LoadableComponent,
+ setComponentLoaded,
+ setUpLoadableComponent
+} from "../../utils/loadable";
import {
connectLocalized,
disconnectLocalized,
@@ -46,7 +53,7 @@ import { DateLocaleData, getLocaleData, getValueAsDateRange } from "./utils";
delegatesFocus: true
}
})
-export class DatePicker implements LocalizedComponent, T9nComponent {
+export class DatePicker implements LocalizedComponent, LoadableComponent, T9nComponent {
//--------------------------------------------------------------------------
//
// Element
@@ -188,6 +195,19 @@ export class DatePicker implements LocalizedComponent, T9nComponent {
@State() endAsDate: Date;
+ //--------------------------------------------------------------------------
+ //
+ // Public Methods
+ //
+ //--------------------------------------------------------------------------
+
+ /** Sets focus on the component's first focusable element. */
+ @Method()
+ async setFocus(): Promise {
+ await componentLoaded(this);
+ this.el.focus();
+ }
+
// --------------------------------------------------------------------------
//
// Lifecycle
@@ -218,12 +238,17 @@ export class DatePicker implements LocalizedComponent, T9nComponent {
}
async componentWillLoad(): Promise {
+ setUpLoadableComponent(this);
await this.loadLocaleData();
this.onMinChanged(this.min);
this.onMaxChanged(this.max);
await setUpMessages(this);
}
+ componentDidLoad(): void {
+ setComponentLoaded(this);
+ }
+
render(): VNode {
const date = dateFromRange(
this.range && Array.isArray(this.valueAsDate) ? this.valueAsDate[0] : this.valueAsDate,
diff --git a/src/components/dropdown/dropdown.e2e.ts b/src/components/dropdown/dropdown.e2e.ts
index 8781da42265..3b2c8014f5c 100644
--- a/src/components/dropdown/dropdown.e2e.ts
+++ b/src/components/dropdown/dropdown.e2e.ts
@@ -1,23 +1,26 @@
import { E2EPage, newE2EPage } from "@stencil/core/testing";
import dedent from "dedent";
import { html } from "../../../support/formatting";
-import { accessible, defaults, disabled, floatingUIOwner, hidden, renders } from "../../tests/commonTests";
+import { focusable, accessible, defaults, disabled, floatingUIOwner, hidden, renders } from "../../tests/commonTests";
import { GlobalTestProps } from "../../tests/utils";
import { CSS } from "./resources";
+const simpleDropdownHTML = html`
+ Open dropdown
+
+ Dropdown Item Content
+ Dropdown Item Content
+ Dropdown Item Content
+
+`;
+
describe("calcite-dropdown", () => {
- it("renders", () =>
- renders(
- html`
- Open dropdown
-
- Dropdown Item Content
- Dropdown Item Content
- Dropdown Item Content
-
- `,
- { display: "inline-flex" }
- ));
+ it("focusable", async () =>
+ focusable(simpleDropdownHTML, {
+ focusTargetSelector: '[slot="trigger"]'
+ }));
+
+ it("renders", () => renders(simpleDropdownHTML, { display: "inline-flex" }));
it("honors hidden attribute", async () => hidden("calcite-dropdown"));
@@ -33,18 +36,7 @@ describe("calcite-dropdown", () => {
}
]));
- it("can be disabled", () =>
- disabled(
- html`
- Open dropdown
-
- Dropdown Item Content
- Dropdown Item Content
- Dropdown Item Content
-
- `,
- { focusTarget: "child" }
- ));
+ it("can be disabled", () => disabled(simpleDropdownHTML, { focusTarget: "child" }));
interface SelectedItemsAssertionOptions {
/**
@@ -748,7 +740,7 @@ describe("calcite-dropdown", () => {
expect(calciteDropdownOpen).toHaveReceivedEventTimes(1);
expect(calciteDropdownClose).toHaveReceivedEventTimes(0);
- await trigger.focus();
+ await element.callMethod("setFocus");
await page.keyboard.press("Space");
await page.waitForChanges();
expect(await dropdownWrapper.isVisible()).toBe(false);
@@ -793,7 +785,7 @@ describe("calcite-dropdown", () => {
expect(calciteDropdownOpen).toHaveReceivedEventTimes(1);
expect(calciteDropdownClose).toHaveReceivedEventTimes(0);
- await trigger.focus();
+ await element.callMethod("setFocus");
await page.keyboard.press("Space");
await page.waitForChanges();
expect(await dropdownWrapper.isVisible()).toBe(false);
diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx
index b77baeb4b0c..cd08c4ca431 100644
--- a/src/components/dropdown/dropdown.tsx
+++ b/src/components/dropdown/dropdown.tsx
@@ -35,6 +35,12 @@ import {
import { guid } from "../../utils/guid";
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
import { isActivationKey } from "../../utils/key";
+import {
+ componentLoaded,
+ LoadableComponent,
+ setComponentLoaded,
+ setUpLoadableComponent
+} from "../../utils/loadable";
import { createObserver } from "../../utils/observers";
import {
connectOpenCloseComponent,
@@ -56,7 +62,9 @@ import { SLOTS } from "./resources";
delegatesFocus: true
}
})
-export class Dropdown implements InteractiveComponent, OpenCloseComponent, FloatingUIComponent {
+export class Dropdown
+ implements InteractiveComponent, LoadableComponent, OpenCloseComponent, FloatingUIComponent
+{
//--------------------------------------------------------------------------
//
// Element
@@ -185,6 +193,19 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float
*/
@Prop({ reflect: true }) width: Scale;
+ //--------------------------------------------------------------------------
+ //
+ // Public Methods
+ //
+ //--------------------------------------------------------------------------
+
+ /** Sets focus on the component's first focusable element. */
+ @Method()
+ async setFocus(): Promise {
+ await componentLoaded(this);
+ this.el.focus();
+ }
+
//--------------------------------------------------------------------------
//
// Lifecycle
@@ -201,7 +222,12 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float
connectOpenCloseComponent(this);
}
+ componentWillLoad(): void {
+ setUpLoadableComponent(this);
+ }
+
componentDidLoad(): void {
+ setComponentLoaded(this);
this.reposition(true);
}
diff --git a/src/components/pagination/pagination.e2e.ts b/src/components/pagination/pagination.e2e.ts
index bea94d99450..558fcf8369e 100644
--- a/src/components/pagination/pagination.e2e.ts
+++ b/src/components/pagination/pagination.e2e.ts
@@ -1,11 +1,21 @@
-import { newE2EPage, E2EElement, E2EPage } from "@stencil/core/testing";
-import { accessible, hidden, renders, t9n } from "../../tests/commonTests";
-import { CSS } from "./resources";
+import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
+import { accessible, focusable, hidden, renders, t9n } from "../../tests/commonTests";
+import { CSS } from "./resources";
describe("calcite-pagination", () => {
it("renders", async () => renders("calcite-pagination", { display: "flex" }));
+ it("focuses previous button when not on the first page", async () =>
+ focusable('', {
+ shadowFocusTargetSelector: `.${CSS.previous}`
+ }));
+
+ it("focuses page number 1 when on the first page", async () =>
+ focusable('', {
+ shadowFocusTargetSelector: `.${CSS.page}`
+ }));
+
it("honors hidden attribute", async () => hidden("calcite-pagination"));
it("is accessible", async () => accessible(``));
diff --git a/src/components/pagination/pagination.tsx b/src/components/pagination/pagination.tsx
index 144afc99417..773eeeba7dd 100644
--- a/src/components/pagination/pagination.tsx
+++ b/src/components/pagination/pagination.tsx
@@ -11,6 +11,12 @@ import {
VNode,
Watch
} from "@stencil/core";
+import {
+ componentLoaded,
+ LoadableComponent,
+ setComponentLoaded,
+ setUpLoadableComponent
+} from "../../utils/loadable";
import {
connectLocalized,
disconnectLocalized,
@@ -44,7 +50,9 @@ export interface PaginationDetail {
},
assetsDirs: ["assets"]
})
-export class Pagination implements LocalizedComponent, LocalizedComponent, T9nComponent {
+export class Pagination
+ implements LocalizedComponent, LocalizedComponent, LoadableComponent, T9nComponent
+{
//--------------------------------------------------------------------------
//
// Public Properties
@@ -145,6 +153,11 @@ export class Pagination implements LocalizedComponent, LocalizedComponent, T9nCo
async componentWillLoad(): Promise {
await setUpMessages(this);
+ setUpLoadableComponent(this);
+ }
+
+ componentDidLoad(): void {
+ setComponentLoaded(this);
}
disconnectedCallback(): void {
@@ -158,6 +171,13 @@ export class Pagination implements LocalizedComponent, LocalizedComponent, T9nCo
//
// --------------------------------------------------------------------------
+ /** Sets focus on the component's first focusable element. */
+ @Method()
+ async setFocus(): Promise {
+ await componentLoaded(this);
+ this.el.focus();
+ }
+
/** Go to the next page of results. */
@Method()
async nextPage(): Promise {
diff --git a/src/components/split-button/split-button.e2e.ts b/src/components/split-button/split-button.e2e.ts
index a649700f3b0..8042d663811 100644
--- a/src/components/split-button/split-button.e2e.ts
+++ b/src/components/split-button/split-button.e2e.ts
@@ -1,6 +1,6 @@
import { newE2EPage } from "@stencil/core/testing";
-import { accessible, renders, defaults, disabled, hidden } from "../../tests/commonTests";
import { html } from "../../../support/formatting";
+import { accessible, defaults, disabled, focusable, hidden, renders } from "../../tests/commonTests";
import { CSS } from "./resources";
describe("calcite-split-button", () => {
@@ -22,6 +22,16 @@ describe("calcite-split-button", () => {
it("honors hidden attribute", async () => hidden("calcite-split-button"));
+ it("focusable", async () =>
+ focusable(
+ `
+ ${content}
+ `,
+ {
+ shadowFocusTargetSelector: "calcite-button"
+ }
+ ));
+
it("is accessible", async () =>
accessible(`;
+ //--------------------------------------------------------------------------
+ //
+ // Public Methods
+ //
+ //--------------------------------------------------------------------------
+
+ /** Sets focus on the component's first focusable element. */
+ @Method()
+ async setFocus(): Promise {
+ await componentLoaded(this);
+ this.el.focus();
+ }
+
//--------------------------------------------------------------------------
//
// Lifecycle
//
//--------------------------------------------------------------------------
+ componentWillLoad(): void {
+ setUpLoadableComponent(this);
+ }
+
+ componentDidLoad(): void {
+ setComponentLoaded(this);
+ }
+
componentDidRender(): void {
updateHostInteraction(this);
}
diff --git a/src/tests/commonTests.ts b/src/tests/commonTests.ts
index 085e53bd076..89289c65286 100644
--- a/src/tests/commonTests.ts
+++ b/src/tests/commonTests.ts
@@ -1,12 +1,12 @@
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
-import { JSX } from "../components";
-import { toHaveNoViolations } from "jest-axe";
import axe from "axe-core";
+import { toHaveNoViolations } from "jest-axe";
import { config } from "../../stencil.config";
-import { GlobalTestProps, skipAnimations } from "./utils";
-import { hiddenFormInputSlotName } from "../utils/form";
import { html } from "../../support/formatting";
+import { JSX } from "../components";
+import { hiddenFormInputSlotName } from "../utils/form";
import { MessageBundle } from "../utils/t9n";
+import { GlobalTestProps, skipAnimations } from "./utils";
expect.extend(toHaveNoViolations);
@@ -213,14 +213,13 @@ export async function focusable(componentTagOrHTML: TagOrHTML, options?: Focusab
const tag = getTag(componentTagOrHTML);
const element = await page.find(tag);
const focusTargetSelector = options?.focusTargetSelector || tag;
-
await element.callMethod("setFocus", options?.focusId); // assumes element is FocusableElement
if (options?.shadowFocusTargetSelector) {
expect(
await page.$eval(
tag,
- (element: HTMLElement, selector: string) => element.shadowRoot.activeElement.matches(selector),
+ (element: HTMLElement, selector: string) => element.shadowRoot.activeElement?.matches(selector),
options?.shadowFocusTargetSelector
)
).toBe(true);
@@ -229,7 +228,7 @@ export async function focusable(componentTagOrHTML: TagOrHTML, options?: Focusab
// wait for next frame before checking focus
await page.waitForTimeout(0);
- expect(await page.evaluate((selector) => document.activeElement.matches(selector), focusTargetSelector)).toBe(true);
+ expect(await page.evaluate((selector) => document.activeElement?.matches(selector), focusTargetSelector)).toBe(true);
}
/**