diff --git a/packages/calcite-components/src/components/accordion-item/accordion-item.tsx b/packages/calcite-components/src/components/accordion-item/accordion-item.tsx index 319e681b250..1b4e5b2e387 100644 --- a/packages/calcite-components/src/components/accordion-item/accordion-item.tsx +++ b/packages/calcite-components/src/components/accordion-item/accordion-item.tsx @@ -112,12 +112,18 @@ export class AccordionItem extends LitElement { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.headerEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/action-bar/action-bar.tsx b/packages/calcite-components/src/components/action-bar/action-bar.tsx index 671880310c3..573ee782fa8 100755 --- a/packages/calcite-components/src/components/action-bar/action-bar.tsx +++ b/packages/calcite-components/src/components/action-bar/action-bar.tsx @@ -172,12 +172,18 @@ export class ActionBar extends LitElement { this.resize({ width: this.el.clientWidth, height: this.el.clientHeight }); } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/action-group/action-group.tsx b/packages/calcite-components/src/components/action-group/action-group.tsx index 29c5c798abb..46df96ff3c1 100755 --- a/packages/calcite-components/src/components/action-group/action-group.tsx +++ b/packages/calcite-components/src/components/action-group/action-group.tsx @@ -98,12 +98,18 @@ export class ActionGroup extends LitElement { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/action-menu/action-menu.tsx b/packages/calcite-components/src/components/action-menu/action-menu.tsx index 874bf53d4b9..63aff4ee8fa 100755 --- a/packages/calcite-components/src/components/action-menu/action-menu.tsx +++ b/packages/calcite-components/src/components/action-menu/action-menu.tsx @@ -180,12 +180,18 @@ export class ActionMenu extends LitElement { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.menuButtonEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/action-pad/action-pad.tsx b/packages/calcite-components/src/components/action-pad/action-pad.tsx index 2b566942787..7b7d7ea2d86 100755 --- a/packages/calcite-components/src/components/action-pad/action-pad.tsx +++ b/packages/calcite-components/src/components/action-pad/action-pad.tsx @@ -100,12 +100,18 @@ export class ActionPad extends LitElement { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/action/action.e2e.ts b/packages/calcite-components/src/components/action/action.e2e.ts index e18cb2bdde9..72c7e5dffe1 100755 --- a/packages/calcite-components/src/components/action/action.e2e.ts +++ b/packages/calcite-components/src/components/action/action.e2e.ts @@ -1,6 +1,17 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, disabled, hidden, renders, slots, t9n, defaults, themed, reflects } from "../../tests/commonTests"; +import { + accessible, + disabled, + hidden, + renders, + slots, + t9n, + defaults, + themed, + reflects, + focusable, +} from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import { CSS, SLOTS } from "./resources"; @@ -111,6 +122,10 @@ describe("calcite-action", () => { disabled("calcite-action"); }); + describe("focusable", () => { + focusable("calcite-action"); + }); + describe("slots", () => { slots("calcite-action", SLOTS); }); diff --git a/packages/calcite-components/src/components/action/action.tsx b/packages/calcite-components/src/components/action/action.tsx index 6e57f13df20..38b673ab50a 100644 --- a/packages/calcite-components/src/components/action/action.tsx +++ b/packages/calcite-components/src/components/action/action.tsx @@ -134,12 +134,18 @@ export class Action extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.buttonEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/alert/alert.tsx b/packages/calcite-components/src/components/alert/alert.tsx index 0209855f2ad..a2a4690ebb8 100644 --- a/packages/calcite-components/src/components/alert/alert.tsx +++ b/packages/calcite-components/src/components/alert/alert.tsx @@ -168,13 +168,17 @@ export class Alert extends LitElement implements OpenCloseComponent { /** * Sets focus on the component's "close" button, the first focusable item. * - * `@returns` {Promise} + * `@returns` {Promise} + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/autocomplete/autocomplete.tsx b/packages/calcite-components/src/components/autocomplete/autocomplete.tsx index 7b714c481da..d89308b2517 100644 --- a/packages/calcite-components/src/components/autocomplete/autocomplete.tsx +++ b/packages/calcite-components/src/components/autocomplete/autocomplete.tsx @@ -366,7 +366,7 @@ export class Autocomplete * top: 0, // Specifies the number of pixels along the Y axis to scroll the window or element * behavior: "auto" // Specifies whether the scrolling should animate smoothly (smooth), or happen instantly in a single jump (auto, the default value). * }); - * @param options - allows specific coordinates to be defined. + * @param options * @returns - promise that resolves once the content is scrolled to. */ @method() @@ -387,13 +387,16 @@ export class Autocomplete /** * Sets focus on the component's first focusable element. * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns {Promise} */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.referenceEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/block-group/block-group.tsx b/packages/calcite-components/src/components/block-group/block-group.tsx index 9eb86eb0be7..a36c76697eb 100755 --- a/packages/calcite-components/src/components/block-group/block-group.tsx +++ b/packages/calcite-components/src/components/block-group/block-group.tsx @@ -113,13 +113,16 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort /** * Sets focus on the component's first focusable element. * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns {Promise} */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/block-section/block-section.tsx b/packages/calcite-components/src/components/block-section/block-section.tsx index ddd123d10a5..f0b7e63bbe8 100644 --- a/packages/calcite-components/src/components/block-section/block-section.tsx +++ b/packages/calcite-components/src/components/block-section/block-section.tsx @@ -96,12 +96,18 @@ export class BlockSection extends LitElement { //#region Public Methods - /** Sets focus on the component's first tabbable element. */ + /** + * Sets focus on the component's first tabbable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/block/block.tsx b/packages/calcite-components/src/components/block/block.tsx index b4312572210..77db2b9d197 100644 --- a/packages/calcite-components/src/components/block/block.tsx +++ b/packages/calcite-components/src/components/block/block.tsx @@ -202,12 +202,18 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose //#region Public Methods - /** Sets focus on the component's first tabbable element. */ + /** + * Sets focus on the component's first tabbable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/button/button.e2e.ts b/packages/calcite-components/src/components/button/button.e2e.ts index 7281b9b90f4..ad4ac2d9783 100644 --- a/packages/calcite-components/src/components/button/button.e2e.ts +++ b/packages/calcite-components/src/components/button/button.e2e.ts @@ -1,7 +1,17 @@ // @ts-strict-ignore import { newE2EPage, E2EElement } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, defaults, disabled, hidden, HYDRATED_ATTR, labelable, t9n, themed } from "../../tests/commonTests"; +import { + accessible, + defaults, + disabled, + focusable, + hidden, + HYDRATED_ATTR, + labelable, + t9n, + themed, +} from "../../tests/commonTests"; import { GlobalTestProps } from "../../tests/utils/puppeteer"; import { html } from "../../../support/formatting"; import { CSS } from "./resources"; @@ -172,6 +182,10 @@ describe("calcite-button", () => { disabled("calcite-button"); }); + describe("focusable", () => { + focusable("calcite-button"); + }); + it("should have aria-live attribute set to polite by default", async () => { const page = await newE2EPage(); await page.setContent(`Continue`); diff --git a/packages/calcite-components/src/components/button/button.tsx b/packages/calcite-components/src/components/button/button.tsx index 5e1a2bba3d7..c29d91dc42e 100644 --- a/packages/calcite-components/src/components/button/button.tsx +++ b/packages/calcite-components/src/components/button/button.tsx @@ -190,10 +190,16 @@ export class Button //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { - return this.focusSetter(() => this.childEl); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.childEl, options); } //#endregion diff --git a/packages/calcite-components/src/components/card-group/card-group.e2e.ts b/packages/calcite-components/src/components/card-group/card-group.e2e.ts index 1e73ac5d9c1..88902540137 100644 --- a/packages/calcite-components/src/components/card-group/card-group.e2e.ts +++ b/packages/calcite-components/src/components/card-group/card-group.e2e.ts @@ -2,7 +2,7 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; import { html } from "../../../support/formatting"; -import { accessible, renders, hidden, disabled, themed } from "../../tests/commonTests"; +import { accessible, renders, hidden, disabled, themed, focusable } from "../../tests/commonTests"; import { createSelectedItemsAsserter } from "../../tests/utils/puppeteer"; import { CSS } from "./resources"; @@ -21,6 +21,18 @@ describe("calcite-card-group", () => { disabled("", { focusTarget: "none" }); }); + describe("focusable", () => { + focusable( + html` + Heading + Heading + `, + { + focusTargetSelector: "calcite-card:first-of-type", + }, + ); + }); + describe("is accessible in selection mode none (default)", () => { accessible( html` @@ -32,7 +44,7 @@ describe("calcite-card-group", () => { describe("is accessible in selection mode single", () => { accessible( - html` + html` Heading Heading `, diff --git a/packages/calcite-components/src/components/card-group/card-group.tsx b/packages/calcite-components/src/components/card-group/card-group.tsx index c31ba705b27..a0a18748c3f 100644 --- a/packages/calcite-components/src/components/card-group/card-group.tsx +++ b/packages/calcite-components/src/components/card-group/card-group.tsx @@ -67,12 +67,18 @@ export class CardGroup extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.items[0]; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/card/card.e2e.ts b/packages/calcite-components/src/components/card/card.e2e.ts index 353ea0e6ab8..04d1688c366 100644 --- a/packages/calcite-components/src/components/card/card.e2e.ts +++ b/packages/calcite-components/src/components/card/card.e2e.ts @@ -1,6 +1,6 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, renders, slots, hidden, t9n, themed } from "../../tests/commonTests"; +import { accessible, renders, slots, hidden, t9n, themed, focusable } from "../../tests/commonTests"; import { placeholderImage } from "../../../.storybook/placeholder-image"; import { html } from "../../../support/formatting"; import { CSS, SLOTS } from "./resources"; @@ -19,6 +19,10 @@ describe("calcite-card", () => { hidden("calcite-card"); }); + describe("focusable", () => { + focusable("calcite-card"); + }); + describe("accessible", () => { accessible("calcite-card"); }); diff --git a/packages/calcite-components/src/components/card/card.tsx b/packages/calcite-components/src/components/card/card.tsx index 1a221936347..17d7ec66ba9 100644 --- a/packages/calcite-components/src/components/card/card.tsx +++ b/packages/calcite-components/src/components/card/card.tsx @@ -126,12 +126,18 @@ export class Card extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.containerEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/carousel/carousel.e2e.ts b/packages/calcite-components/src/components/carousel/carousel.e2e.ts index 4867077992d..c983457cada 100644 --- a/packages/calcite-components/src/components/carousel/carousel.e2e.ts +++ b/packages/calcite-components/src/components/carousel/carousel.e2e.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, hidden, renders, t9n, themed } from "../../tests/commonTests"; +import { accessible, focusable, hidden, renders, t9n, themed } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import { breakpoints } from "../../utils/responsive"; import { findAll } from "../../tests/utils/puppeteer"; @@ -35,6 +35,17 @@ describe("calcite-carousel", () => { ); }); + describe("focusable", () => { + focusable( + html`

carousel item content

carousel item content

`, + ); + }); + describe("accessible", () => { accessible( html` { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.container; - }); + }, options); } /** Stop the carousel. If `autoplay` is not enabled (initialized either to `true` or `"paused"`), these methods will have no effect. */ diff --git a/packages/calcite-components/src/components/checkbox/checkbox.tsx b/packages/calcite-components/src/components/checkbox/checkbox.tsx index 103f0258979..f43f0ac4440 100644 --- a/packages/calcite-components/src/components/checkbox/checkbox.tsx +++ b/packages/calcite-components/src/components/checkbox/checkbox.tsx @@ -143,12 +143,18 @@ export class Checkbox // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.toggleEl.value; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/chip-group/chip-group.e2e.ts b/packages/calcite-components/src/components/chip-group/chip-group.e2e.ts index 7ed547928cd..3bd0ed8e2bb 100644 --- a/packages/calcite-components/src/components/chip-group/chip-group.e2e.ts +++ b/packages/calcite-components/src/components/chip-group/chip-group.e2e.ts @@ -2,7 +2,7 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; import { html } from "../../../support/formatting"; -import { accessible, renders, hidden, disabled } from "../../tests/commonTests"; +import { accessible, renders, hidden, disabled, focusable } from "../../tests/commonTests"; import { CSS as CHIP_CSS } from "../chip/resources"; import { createSelectedItemsAsserter } from "../../tests/utils/puppeteer"; @@ -23,6 +23,18 @@ describe("calcite-chip-group", () => { }); }); + describe("focusable", () => { + focusable( + html` + + + `, + { + focusTargetSelector: "calcite-chip:first-of-type", + }, + ); + }); + describe("is accessible in selection mode none (default)", () => { accessible( html` @@ -34,14 +46,14 @@ describe("calcite-chip-group", () => { describe("is accessible in selection mode single", () => { accessible( - html` + html` `, ); }); - describe("is selection mode single persists", () => { + describe("is accessible in selection mode single persists", () => { accessible( html` diff --git a/packages/calcite-components/src/components/chip-group/chip-group.tsx b/packages/calcite-components/src/components/chip-group/chip-group.tsx index f3060a380b3..e13d571302b 100644 --- a/packages/calcite-components/src/components/chip-group/chip-group.tsx +++ b/packages/calcite-components/src/components/chip-group/chip-group.tsx @@ -82,12 +82,18 @@ export class ChipGroup extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.selectedItems[0] || this.items[0]; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/chip/chip.tsx b/packages/calcite-components/src/components/chip/chip.tsx index 28746b6d246..be30ecbdd9b 100644 --- a/packages/calcite-components/src/components/chip/chip.tsx +++ b/packages/calcite-components/src/components/chip/chip.tsx @@ -135,16 +135,22 @@ export class Chip extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { if (this.interactive) { return this.containerEl.value; } else if (this.closable) { return this.closeButtonEl.value; } - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/color-picker-hex-input/color-picker-hex-input.tsx b/packages/calcite-components/src/components/color-picker-hex-input/color-picker-hex-input.tsx index 463e95a8310..158bbc0a03b 100644 --- a/packages/calcite-components/src/components/color-picker-hex-input/color-picker-hex-input.tsx +++ b/packages/calcite-components/src/components/color-picker-hex-input/color-picker-hex-input.tsx @@ -101,12 +101,18 @@ export class ColorPickerHexInput extends LitElement { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.hexInputNode; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/color-picker/color-picker.tsx b/packages/calcite-components/src/components/color-picker/color-picker.tsx index 72e9d9181d9..96ba7998b7b 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.tsx +++ b/packages/calcite-components/src/components/color-picker/color-picker.tsx @@ -372,12 +372,18 @@ export class ColorPicker extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/combobox/combobox.e2e.ts b/packages/calcite-components/src/components/combobox/combobox.e2e.ts index d7e6e1f8af2..b20ae4a6fad 100644 --- a/packages/calcite-components/src/components/combobox/combobox.e2e.ts +++ b/packages/calcite-components/src/components/combobox/combobox.e2e.ts @@ -6,6 +6,7 @@ import { defaults, disabled, floatingUIOwner, + focusable, formAssociated, hidden, labelable, @@ -140,6 +141,15 @@ describe("calcite-combobox", () => { ]); }); + describe("focusable", () => { + focusable(html` + + + + + `); + }); + describe("honors hidden attribute", () => { hidden("calcite-combobox"); }); diff --git a/packages/calcite-components/src/components/combobox/combobox.tsx b/packages/calcite-components/src/components/combobox/combobox.tsx index ae60c990c26..6b7003688ef 100644 --- a/packages/calcite-components/src/components/combobox/combobox.tsx +++ b/packages/calcite-components/src/components/combobox/combobox.tsx @@ -507,14 +507,20 @@ export class Combobox ); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { this.activeChipIndex = -1; this.activeItemIndex = -1; return this.textInput.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/date-picker-day/date-picker-day.e2e.ts b/packages/calcite-components/src/components/date-picker-day/date-picker-day.e2e.ts index 10e7cb397da..8a08b24ed87 100644 --- a/packages/calcite-components/src/components/date-picker-day/date-picker-day.e2e.ts +++ b/packages/calcite-components/src/components/date-picker-day/date-picker-day.e2e.ts @@ -1,6 +1,6 @@ import { E2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it, beforeEach } from "vitest"; -import { disabled } from "../../tests/commonTests"; +import { disabled, focusable } from "../../tests/commonTests"; import { newProgrammaticE2EPage } from "../../tests/utils/puppeteer"; import { DATE_PICKER_FORMAT_OPTIONS } from "../date-picker/resources"; @@ -23,6 +23,22 @@ describe("calcite-date-picker-day", () => { disabled(() => ({ tag: "calcite-date-picker-day", page })); }); + describe("focusable", () => { + focusable(async () => { + const page = await newProgrammaticE2EPage(); + await page.evaluate(() => { + const dateEl = document.createElement("calcite-date-picker-day"); + dateEl.active = true; + dateEl.dateTimeFormat = new Intl.DateTimeFormat("en"); // options not needed as this is only needed for rendering + dateEl.day = 3; + document.body.append(dateEl); + }); + await page.waitForChanges(); + + return { tag: "calcite-date-picker-day", page }; + }); + }); + describe("accessibility", () => { it("labels its associated day", async () => { const page = await newProgrammaticE2EPage(); diff --git a/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx b/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx index f1945070311..ea2462f88e3 100644 --- a/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx +++ b/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx @@ -104,12 +104,18 @@ export class DatePickerDay extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/date-picker/date-picker.tsx b/packages/calcite-components/src/components/date-picker/date-picker.tsx index 58f32bf76d2..bd4c9800507 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.tsx +++ b/packages/calcite-components/src/components/date-picker/date-picker.tsx @@ -155,12 +155,18 @@ export class DatePicker extends LitElement { this.rangeValueChangedByUser = false; } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/dialog/dialog.e2e.ts b/packages/calcite-components/src/components/dialog/dialog.e2e.ts index 5b89686a390..ff488db736e 100644 --- a/packages/calcite-components/src/components/dialog/dialog.e2e.ts +++ b/packages/calcite-components/src/components/dialog/dialog.e2e.ts @@ -704,7 +704,7 @@ describe("calcite-dialog", () => { }); }); - describe("setFocus", () => { + describe("focusable", () => { const createDialogHTML = (contentHTML?: string, attrs?: string) => `${contentHTML}`; diff --git a/packages/calcite-components/src/components/dialog/dialog.tsx b/packages/calcite-components/src/components/dialog/dialog.tsx index 3fe9b0831de..c01fdbdc1b6 100644 --- a/packages/calcite-components/src/components/dialog/dialog.tsx +++ b/packages/calcite-components/src/components/dialog/dialog.tsx @@ -254,7 +254,7 @@ export class Dialog extends LitElement implements OpenCloseComponent { * top: 0, // Specifies the number of pixels along the Y axis to scroll the window or element * behavior: "auto" // Specifies whether the scrolling should animate smoothly (smooth), or happen instantly in a single jump (auto, the default value). * }); - * @param options - allows specific coordinates to be defined. + * @param options * @returns - promise that resolves once the content is scrolled to. */ @method() @@ -265,13 +265,16 @@ export class Dialog extends LitElement implements OpenCloseComponent { /** * Sets focus on the component's "close" button (the first focusable item). * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns {Promise} - A promise that is resolved when the operation has completed. */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.panelEl.value ?? this.el; - }); + }, options); } /** 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 bb14a733a0b..b6385d03677 100644 --- a/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx +++ b/packages/calcite-components/src/components/dropdown-item/dropdown-item.tsx @@ -111,12 +111,18 @@ export class DropdownItem extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/dropdown/dropdown.tsx b/packages/calcite-components/src/components/dropdown/dropdown.tsx index 0ff53e03b2d..2fccaefc596 100644 --- a/packages/calcite-components/src/components/dropdown/dropdown.tsx +++ b/packages/calcite-components/src/components/dropdown/dropdown.tsx @@ -201,12 +201,18 @@ export class Dropdown ); } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.referenceEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/fab/fab.e2e.ts b/packages/calcite-components/src/components/fab/fab.e2e.ts index c90939beecd..40e700dbb51 100755 --- a/packages/calcite-components/src/components/fab/fab.e2e.ts +++ b/packages/calcite-components/src/components/fab/fab.e2e.ts @@ -1,6 +1,6 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, defaults, disabled, hidden, renders, themed } from "../../tests/commonTests"; +import { accessible, defaults, disabled, focusable, hidden, renders, themed } from "../../tests/commonTests"; import { findAll } from "../../tests/utils/puppeteer"; import { html } from "../../../support/formatting"; import { CSS } from "./resources"; @@ -31,6 +31,10 @@ describe("calcite-fab", () => { disabled("calcite-fab"); }); + describe("focusable", () => { + focusable("calcite-fab"); + }); + it(`should set all internal calcite-button types to 'button'`, async () => { const page = await newE2EPage({ html: "", diff --git a/packages/calcite-components/src/components/fab/fab.tsx b/packages/calcite-components/src/components/fab/fab.tsx index cbe93ddf889..c5ddf5a8fd6 100755 --- a/packages/calcite-components/src/components/fab/fab.tsx +++ b/packages/calcite-components/src/components/fab/fab.tsx @@ -75,12 +75,18 @@ export class Fab extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.buttonEl.value; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/filter/filter.tsx b/packages/calcite-components/src/components/filter/filter.tsx index f5246b68422..5905566a5c6 100644 --- a/packages/calcite-components/src/components/filter/filter.tsx +++ b/packages/calcite-components/src/components/filter/filter.tsx @@ -131,12 +131,18 @@ export class Filter extends LitElement implements InteractiveComponent { }); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.textInput.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/flow-item/flow-item.tsx b/packages/calcite-components/src/components/flow-item/flow-item.tsx index 7f61f88e511..78df6ae75db 100644 --- a/packages/calcite-components/src/components/flow-item/flow-item.tsx +++ b/packages/calcite-components/src/components/flow-item/flow-item.tsx @@ -166,13 +166,16 @@ export class FlowItem extends LitElement implements InteractiveComponent { /** * Sets focus on the component. * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns promise. */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.backButtonEl || this.containerEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/flow/flow.e2e.ts b/packages/calcite-components/src/components/flow/flow.e2e.ts index 6e4fd4dbdd7..3471855e7e8 100755 --- a/packages/calcite-components/src/components/flow/flow.e2e.ts +++ b/packages/calcite-components/src/components/flow/flow.e2e.ts @@ -512,8 +512,8 @@ describe("calcite-flow", () => { // no op } - async setFocus(): Promise { - await this.flowItemEl.setFocus(); + async setFocus(options?: FocusOptions): Promise { + await this.flowItemEl.setFocus(options); } } diff --git a/packages/calcite-components/src/components/flow/flow.tsx b/packages/calcite-components/src/components/flow/flow.tsx index 0b121523f32..ab497022438 100755 --- a/packages/calcite-components/src/components/flow/flow.tsx +++ b/packages/calcite-components/src/components/flow/flow.tsx @@ -93,13 +93,16 @@ export class Flow extends LitElement { /** * Sets focus on the component. * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns Promise */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.items[this.selectedIndex]; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/handle/handle.e2e.ts b/packages/calcite-components/src/components/handle/handle.e2e.ts index 560fb557cf4..d6398ff7515 100644 --- a/packages/calcite-components/src/components/handle/handle.e2e.ts +++ b/packages/calcite-components/src/components/handle/handle.e2e.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, disabled, hidden, renders, themed, t9n } from "../../tests/commonTests"; +import { accessible, disabled, hidden, renders, themed, t9n, focusable } from "../../tests/commonTests"; import { CSS, SUBSTITUTIONS } from "./resources"; import type { HandleNudge } from "./interfaces"; import type { Handle } from "./handle"; @@ -19,6 +19,10 @@ describe("calcite-handle", () => { disabled("calcite-handle"); }); + describe("focusable", () => { + focusable("calcite-handle"); + }); + describe("accessible", () => { accessible(``); }); diff --git a/packages/calcite-components/src/components/handle/handle.tsx b/packages/calcite-components/src/components/handle/handle.tsx index cb4e4daabd6..a37813699c7 100644 --- a/packages/calcite-components/src/components/handle/handle.tsx +++ b/packages/calcite-components/src/components/handle/handle.tsx @@ -89,12 +89,18 @@ export class Handle extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.handleButton.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/inline-editable/inline-editable.e2e.ts b/packages/calcite-components/src/components/inline-editable/inline-editable.e2e.ts index 42a22735967..cefa0bc4b30 100644 --- a/packages/calcite-components/src/components/inline-editable/inline-editable.e2e.ts +++ b/packages/calcite-components/src/components/inline-editable/inline-editable.e2e.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { E2EPage, newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { beforeEach, describe, expect, it } from "vitest"; -import { accessible, disabled, hidden, labelable, renders, t9n, themed } from "../../tests/commonTests"; +import { accessible, disabled, focusable, hidden, labelable, renders, t9n, themed } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import type { Input } from "../input/input"; import { findAll } from "../../tests/utils/puppeteer"; @@ -36,6 +36,19 @@ describe("calcite-inline-editable", () => { ); }); + describe("focusable", () => { + focusable( + html` + + + + `, + { + focusTargetSelector: "calcite-input", + }, + ); + }); + describe("rendering permutations", () => { it("renders default props when none are provided", async () => { const page: E2EPage = await newE2EPage(); diff --git a/packages/calcite-components/src/components/inline-editable/inline-editable.tsx b/packages/calcite-components/src/components/inline-editable/inline-editable.tsx index 575e2424be9..0b6e71a6889 100644 --- a/packages/calcite-components/src/components/inline-editable/inline-editable.tsx +++ b/packages/calcite-components/src/components/inline-editable/inline-editable.tsx @@ -101,12 +101,18 @@ export class InlineEditable extends LitElement implements InteractiveComponent, //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.inputElement; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx index afa61ded883..faefabbb658 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx @@ -370,12 +370,18 @@ export class InputDatePicker ); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input-number/input-number.tsx b/packages/calcite-components/src/components/input-number/input-number.tsx index 12bc303ea79..0994c63ec2e 100644 --- a/packages/calcite-components/src/components/input-number/input-number.tsx +++ b/packages/calcite-components/src/components/input-number/input-number.tsx @@ -360,12 +360,18 @@ export class InputNumber this.childNumberEl?.select(); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.childNumberEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input-text/input-text.tsx b/packages/calcite-components/src/components/input-text/input-text.tsx index 417a07aab86..5d5404f1e57 100644 --- a/packages/calcite-components/src/components/input-text/input-text.tsx +++ b/packages/calcite-components/src/components/input-text/input-text.tsx @@ -293,12 +293,18 @@ export class InputText this.childEl?.select(); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.childEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx b/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx index a515fbea659..095db25fd84 100644 --- a/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx +++ b/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx @@ -237,12 +237,18 @@ export class InputTimePicker this.popoverEl?.reposition(delayed); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx index 8171128e671..8e573657d85 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx @@ -231,12 +231,18 @@ export class InputTimeZone //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.comboboxEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/input/input.tsx b/packages/calcite-components/src/components/input/input.tsx index e852c825ab4..6fa9dcfbd46 100644 --- a/packages/calcite-components/src/components/input/input.tsx +++ b/packages/calcite-components/src/components/input/input.tsx @@ -417,12 +417,18 @@ export class Input } } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.type === "number" ? this.childNumberEl : this.childEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/link/link.e2e.ts b/packages/calcite-components/src/components/link/link.e2e.ts index b79eb37f02c..1643b1d934b 100644 --- a/packages/calcite-components/src/components/link/link.e2e.ts +++ b/packages/calcite-components/src/components/link/link.e2e.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { newE2EPage, E2EPage, E2EElement } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it, beforeEach } from "vitest"; -import { accessible, defaults, disabled, hidden, renders, themed } from "../../tests/commonTests"; +import { accessible, defaults, disabled, focusable, hidden, renders, themed } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import { CSS } from "./resources"; @@ -33,6 +33,16 @@ describe("calcite-link", () => { disabled(`link`); }); + describe("focusable", () => { + describe("default", () => { + focusable(html`link`); + }); + + describe("with href", () => { + focusable(html`link`); + }); + }); + it("sets download attribute on internal anchor", async () => { const page = await newE2EPage(); await page.setContent(`Continue`); diff --git a/packages/calcite-components/src/components/link/link.tsx b/packages/calcite-components/src/components/link/link.tsx index e5824cb896b..5d4511f7a80 100644 --- a/packages/calcite-components/src/components/link/link.tsx +++ b/packages/calcite-components/src/components/link/link.tsx @@ -80,12 +80,18 @@ export class Link extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.childEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/list-item/list-item.tsx b/packages/calcite-components/src/components/list-item/list-item.tsx index 7d2db6b8c42..52c29848928 100644 --- a/packages/calcite-components/src/components/list-item/list-item.tsx +++ b/packages/calcite-components/src/components/list-item/list-item.tsx @@ -257,9 +257,15 @@ export class ListItem extends LitElement implements InteractiveComponent, Sortab //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { const { containerEl: { value: containerEl }, @@ -278,7 +284,7 @@ export class ListItem extends LitElement implements InteractiveComponent, Sortab } return { target: containerEl, includeContainer: true, strategy: "focusable" }; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx index 9499134c262..745f6dc63a5 100755 --- a/packages/calcite-components/src/components/list/list.tsx +++ b/packages/calcite-components/src/components/list/list.tsx @@ -286,15 +286,18 @@ export class List extends LitElement implements InteractiveComponent, SortableCo /** * Sets focus on the component's first focusable element. * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) * @returns {Promise} */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.filterEnabled ? this.filterEl : this.focusableItems.find((listItem) => listItem.active); - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/menu-item/menu-item.tsx b/packages/calcite-components/src/components/menu-item/menu-item.tsx index 23624ac52d9..f4fadb1acab 100644 --- a/packages/calcite-components/src/components/menu-item/menu-item.tsx +++ b/packages/calcite-components/src/components/menu-item/menu-item.tsx @@ -126,12 +126,18 @@ export class MenuItem extends LitElement { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.anchorEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/menu/menu.tsx b/packages/calcite-components/src/components/menu/menu.tsx index fc75bf69c6c..d3b98c0dc6a 100644 --- a/packages/calcite-components/src/components/menu/menu.tsx +++ b/packages/calcite-components/src/components/menu/menu.tsx @@ -62,12 +62,18 @@ export class Menu extends LitElement { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.menuItems[0]; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts index 4842781d7d0..e54d6360dc2 100644 --- a/packages/calcite-components/src/components/modal/modal.e2e.ts +++ b/packages/calcite-components/src/components/modal/modal.e2e.ts @@ -388,7 +388,7 @@ describe("calcite-modal", () => { }); }); - describe("setFocus", () => { + describe("focusable", () => { const createModalHTML = (contentHTML?: string, attrs?: string) => `${contentHTML}`; diff --git a/packages/calcite-components/src/components/modal/modal.tsx b/packages/calcite-components/src/components/modal/modal.tsx index d97a5539749..f22606ddd6c 100644 --- a/packages/calcite-components/src/components/modal/modal.tsx +++ b/packages/calcite-components/src/components/modal/modal.tsx @@ -251,12 +251,18 @@ export class Modal extends LitElement implements OpenCloseComponent { } } - /** Sets focus on the component's "close" button (the first focusable item). */ + /** + * Sets focus on the component's "close" button (the first focusable item). + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } /** diff --git a/packages/calcite-components/src/components/navigation-logo/navigation-logo.tsx b/packages/calcite-components/src/components/navigation-logo/navigation-logo.tsx index 2200c2489e1..d7098f96d30 100644 --- a/packages/calcite-components/src/components/navigation-logo/navigation-logo.tsx +++ b/packages/calcite-components/src/components/navigation-logo/navigation-logo.tsx @@ -74,14 +74,20 @@ export class NavigationLogo extends LitElement { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { if (this.href) { return this.el; } - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/navigation-user/navigation-user.tsx b/packages/calcite-components/src/components/navigation-user/navigation-user.tsx index 8fa9c559773..d78d64f7f9b 100644 --- a/packages/calcite-components/src/components/navigation-user/navigation-user.tsx +++ b/packages/calcite-components/src/components/navigation-user/navigation-user.tsx @@ -52,12 +52,18 @@ export class NavigationUser extends LitElement { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/navigation/navigation.tsx b/packages/calcite-components/src/components/navigation/navigation.tsx index 252436ba9be..45af2292194 100644 --- a/packages/calcite-components/src/components/navigation/navigation.tsx +++ b/packages/calcite-components/src/components/navigation/navigation.tsx @@ -82,12 +82,18 @@ export class Navigation extends LitElement { // #region Public Methods - /** When `navigationAction` is `true`, sets focus on the component's action element. */ + /** + * When `navigationAction` is `true`, sets focus on the component's action element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.navigationActionEl.value; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/notice/notice.tsx b/packages/calcite-components/src/components/notice/notice.tsx index b21da30af47..c2d0f8ae36c 100644 --- a/packages/calcite-components/src/components/notice/notice.tsx +++ b/packages/calcite-components/src/components/notice/notice.tsx @@ -109,13 +109,19 @@ export class Notice extends LitElement implements OpenCloseComponent { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { const noticeLinkEl = this.el.querySelector("calcite-link"); return noticeLinkEl || this.closeButton.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/pagination/pagination.tsx b/packages/calcite-components/src/components/pagination/pagination.tsx index 9d04c6b47b5..96e59bd15ce 100644 --- a/packages/calcite-components/src/components/pagination/pagination.tsx +++ b/packages/calcite-components/src/components/pagination/pagination.tsx @@ -141,12 +141,18 @@ export class Pagination extends LitElement { this.startItem = Math.max(1, this.startItem - this.pageSize); } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/panel/panel.e2e.ts b/packages/calcite-components/src/components/panel/panel.e2e.ts index 945a4821dcf..342c7f0a54c 100644 --- a/packages/calcite-components/src/components/panel/panel.e2e.ts +++ b/packages/calcite-components/src/components/panel/panel.e2e.ts @@ -473,7 +473,7 @@ describe("calcite-panel", () => { `); }); - describe("is focusable", () => { + describe("focusable", () => { describe("with scrolling content", () => { describe("closable", () => { focusable( diff --git a/packages/calcite-components/src/components/panel/panel.tsx b/packages/calcite-components/src/components/panel/panel.tsx index 04e96334adb..5485afe29d5 100644 --- a/packages/calcite-components/src/components/panel/panel.tsx +++ b/packages/calcite-components/src/components/panel/panel.tsx @@ -205,12 +205,18 @@ export class Panel extends LitElement implements InteractiveComponent { this.panelScrollEl?.scrollTo(options); } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.containerEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/popover/popover.tsx b/packages/calcite-components/src/components/popover/popover.tsx index 0d501e05a27..bdbd3c87cbf 100644 --- a/packages/calcite-components/src/components/popover/popover.tsx +++ b/packages/calcite-components/src/components/popover/popover.tsx @@ -250,12 +250,18 @@ export class Popover extends LitElement implements FloatingUIComponent, OpenClos ); } - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } /** Updates the element(s) that are used within the focus-trap of the component. */ diff --git a/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx b/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx index 451cace1f90..e05ba507959 100644 --- a/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx +++ b/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx @@ -95,16 +95,22 @@ export class RadioButtonGroup extends LitElement { // #region Public Methods - /** Sets focus on the fist focusable `calcite-radio-button` element in the component. */ + /** + * Sets focus on the fist focusable `calcite-radio-button` element in the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { if (this.selectedItem && !this.selectedItem.disabled) { return this.selectedItem; } return this.getFocusableRadioButton(); - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/radio-button/radio-button.tsx b/packages/calcite-components/src/components/radio-button/radio-button.tsx index 825cf0f070a..806b6b514e7 100644 --- a/packages/calcite-components/src/components/radio-button/radio-button.tsx +++ b/packages/calcite-components/src/components/radio-button/radio-button.tsx @@ -124,12 +124,18 @@ export class RadioButton this.calciteInternalRadioButtonCheckedChange.emit(); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.containerEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/rating/rating.tsx b/packages/calcite-components/src/components/rating/rating.tsx index fabe32f5524..6d2a9aa9b17 100644 --- a/packages/calcite-components/src/components/rating/rating.tsx +++ b/packages/calcite-components/src/components/rating/rating.tsx @@ -185,12 +185,18 @@ export class Rating //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.tsx b/packages/calcite-components/src/components/segmented-control/segmented-control.tsx index dafe4ba616e..26111ce8d8a 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.tsx +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.tsx @@ -150,12 +150,18 @@ export class SegmentedControl // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.selectedItem || this.items[0]; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/select/select.tsx b/packages/calcite-components/src/components/select/select.tsx index 7269429fc50..1f76ae4f986 100644 --- a/packages/calcite-components/src/components/select/select.tsx +++ b/packages/calcite-components/src/components/select/select.tsx @@ -164,12 +164,18 @@ export class Select // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.selectEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/sheet/sheet.tsx b/packages/calcite-components/src/components/sheet/sheet.tsx index 926c6b11d4b..8863f782c06 100644 --- a/packages/calcite-components/src/components/sheet/sheet.tsx +++ b/packages/calcite-components/src/components/sheet/sheet.tsx @@ -227,12 +227,18 @@ export class Sheet extends LitElement implements OpenCloseComponent { //#region Public Methods - /** Sets focus on the component's "close" button - the first focusable item. */ + /** + * Sets focus on the component's "close" button - the first focusable item. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } /** diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index cf14a0f7add..a9f07042c13 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -324,12 +324,18 @@ export class Slider // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.minHandle || this.maxHandle; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts b/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts index 7927dd96c24..0c729ace1cb 100644 --- a/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts +++ b/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, disabled, hidden, renders, t9n, openClose } from "../../tests/commonTests"; +import { accessible, disabled, hidden, renders, t9n, openClose, focusable } from "../../tests/commonTests"; import { skipAnimations } from "../../tests/utils/puppeteer"; import T9nStrings from "./assets/t9n/messages.en.json"; import { CSS, REORDER_VALUES, SUBSTITUTIONS } from "./resources"; @@ -21,6 +21,10 @@ describe("calcite-sort-handle", () => { disabled(``); }); + describe("focusable", () => { + focusable("calcite-sort-handle"); + }); + describe("accessible", () => { accessible(``); }); diff --git a/packages/calcite-components/src/components/sort-handle/sort-handle.tsx b/packages/calcite-components/src/components/sort-handle/sort-handle.tsx index 00392c4c3a8..f5596663484 100644 --- a/packages/calcite-components/src/components/sort-handle/sort-handle.tsx +++ b/packages/calcite-components/src/components/sort-handle/sort-handle.tsx @@ -117,12 +117,18 @@ export class SortHandle extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.dropdownEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/split-button/split-button.tsx b/packages/calcite-components/src/components/split-button/split-button.tsx index ac4515e28d4..f44ada708e0 100644 --- a/packages/calcite-components/src/components/split-button/split-button.tsx +++ b/packages/calcite-components/src/components/split-button/split-button.tsx @@ -153,12 +153,18 @@ export class SplitButton extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/stepper-item/stepper-item.e2e.ts b/packages/calcite-components/src/components/stepper-item/stepper-item.e2e.ts index 1cfe97fb768..28642e354ce 100644 --- a/packages/calcite-components/src/components/stepper-item/stepper-item.e2e.ts +++ b/packages/calcite-components/src/components/stepper-item/stepper-item.e2e.ts @@ -1,6 +1,6 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { disabled, renders, hidden, t9n, themed } from "../../tests/commonTests"; +import { disabled, renders, hidden, t9n, themed, focusable } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import { CSS } from "./resources"; @@ -17,6 +17,10 @@ describe("calcite-stepper-item", () => { disabled("calcite-stepper-item"); }); + describe("focusable", () => { + focusable(html``); + }); + describe("translation support", () => { t9n(html``); }); diff --git a/packages/calcite-components/src/components/stepper-item/stepper-item.tsx b/packages/calcite-components/src/components/stepper-item/stepper-item.tsx index f2145ab61c0..7e507cfae1e 100644 --- a/packages/calcite-components/src/components/stepper-item/stepper-item.tsx +++ b/packages/calcite-components/src/components/stepper-item/stepper-item.tsx @@ -146,12 +146,18 @@ export class StepperItem extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.layout === "vertical" ? this.el : this.headerEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/switch/switch.e2e.ts b/packages/calcite-components/src/components/switch/switch.e2e.ts index 20d898efda9..3f4d3ecd0af 100644 --- a/packages/calcite-components/src/components/switch/switch.e2e.ts +++ b/packages/calcite-components/src/components/switch/switch.e2e.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { accessible, disabled, + focusable, formAssociated, hidden, HYDRATED_ATTR, @@ -48,6 +49,10 @@ describe("calcite-switch", () => { disabled("calcite-switch"); }); + describe("focusable", () => { + focusable("calcite-switch"); + }); + it("toggles the checked attributes appropriately when clicked", async () => { const page = await newE2EPage(); await page.setContent(""); diff --git a/packages/calcite-components/src/components/switch/switch.tsx b/packages/calcite-components/src/components/switch/switch.tsx index fe014cd2ca9..123f26eefcc 100644 --- a/packages/calcite-components/src/components/switch/switch.tsx +++ b/packages/calcite-components/src/components/switch/switch.tsx @@ -86,12 +86,18 @@ export class Switch // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.switchEl; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/table-cell/table-cell.e2e.ts b/packages/calcite-components/src/components/table-cell/table-cell.e2e.ts new file mode 100644 index 00000000000..56223723162 --- /dev/null +++ b/packages/calcite-components/src/components/table-cell/table-cell.e2e.ts @@ -0,0 +1,8 @@ +import { describe } from "vitest"; +import { focusable } from "../../tests/commonTests"; + +describe("calcite-table-header", () => { + describe("focusable", () => { + focusable("calcite-table-cell"); + }); +}); diff --git a/packages/calcite-components/src/components/table-cell/table-cell.tsx b/packages/calcite-components/src/components/table-cell/table-cell.tsx index e14dad91133..42f6f3d1e1d 100644 --- a/packages/calcite-components/src/components/table-cell/table-cell.tsx +++ b/packages/calcite-components/src/components/table-cell/table-cell.tsx @@ -110,12 +110,18 @@ export class TableCell extends LitElement implements InteractiveComponent { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.containerEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/table-header/table-header.e2e.ts b/packages/calcite-components/src/components/table-header/table-header.e2e.ts new file mode 100644 index 00000000000..5a90c65fe09 --- /dev/null +++ b/packages/calcite-components/src/components/table-header/table-header.e2e.ts @@ -0,0 +1,8 @@ +import { describe } from "vitest"; +import { focusable } from "../../tests/commonTests"; + +describe("calcite-table-header", () => { + describe("focusable", () => { + focusable("calcite-table-header"); + }); +}); diff --git a/packages/calcite-components/src/components/table-header/table-header.tsx b/packages/calcite-components/src/components/table-header/table-header.tsx index 23d0e8f3656..d857da73d63 100644 --- a/packages/calcite-components/src/components/table-header/table-header.tsx +++ b/packages/calcite-components/src/components/table-header/table-header.tsx @@ -110,12 +110,18 @@ export class TableHeader extends LitElement { //#region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.containerEl.value; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/text-area/text-area.tsx b/packages/calcite-components/src/components/text-area/text-area.tsx index bc348196c8d..2ee09ffeb9e 100644 --- a/packages/calcite-components/src/components/text-area/text-area.tsx +++ b/packages/calcite-components/src/components/text-area/text-area.tsx @@ -302,12 +302,18 @@ export class TextArea this.textAreaEl.select(); } - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.textAreaEl; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/components/tile-select/tile-select.tsx b/packages/calcite-components/src/components/tile-select/tile-select.tsx index 13cdbbd8615..25568544704 100644 --- a/packages/calcite-components/src/components/tile-select/tile-select.tsx +++ b/packages/calcite-components/src/components/tile-select/tile-select.tsx @@ -99,12 +99,18 @@ export class TileSelect extends LitElement implements InteractiveComponent { // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.input; - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/tile/tile.tsx b/packages/calcite-components/src/components/tile/tile.tsx index 9479e458ff5..db1581c0515 100644 --- a/packages/calcite-components/src/components/tile/tile.tsx +++ b/packages/calcite-components/src/components/tile/tile.tsx @@ -150,14 +150,20 @@ export class Tile extends LitElement implements InteractiveComponent, Selectable // #region Public Methods - /** Sets focus on the component. */ + /** + * Sets focus on the component. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { if (this.interactive) { return this.containerEl; } - }); + }, options); } // #endregion diff --git a/packages/calcite-components/src/components/time-picker/time-picker.tsx b/packages/calcite-components/src/components/time-picker/time-picker.tsx index 434ca2035c4..22e4965433e 100644 --- a/packages/calcite-components/src/components/time-picker/time-picker.tsx +++ b/packages/calcite-components/src/components/time-picker/time-picker.tsx @@ -105,12 +105,18 @@ export class TimePicker extends LitElement implements TimeComponent { //#region Public Methods - /** Sets focus on the component's first focusable element. */ + /** + * Sets focus on the component's first focusable element. + * + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) + */ @method() - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return this.el; - }); + }, options); } //#endregion diff --git a/packages/calcite-components/src/controllers/useSetFocus.browser.spec.tsx b/packages/calcite-components/src/controllers/useSetFocus.browser.spec.tsx index f09ac84ca30..f865fe4a564 100644 --- a/packages/calcite-components/src/controllers/useSetFocus.browser.spec.tsx +++ b/packages/calcite-components/src/controllers/useSetFocus.browser.spec.tsx @@ -11,8 +11,8 @@ describe("useSetFocus", () => { private focusSetter = useSetFocus()(this); private inputRef = createRef(); - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -32,8 +32,8 @@ describe("useSetFocus", () => { private focusSetter = useSetFocus()(this); private inputRef = createRef>(); - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -52,8 +52,8 @@ describe("useSetFocus", () => { class Test extends LitElement { private focusSetter = useSetFocus()(this); - async setFocus(): Promise { - return this.focusSetter(() => this.el); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.el, options); } override render(): JsxNode { @@ -74,8 +74,8 @@ describe("useSetFocus", () => { disabled = true; - async setFocus(): Promise { - return this.focusSetter(() => this.el); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.el, options); } override render(): JsxNode { @@ -99,8 +99,8 @@ describe("useSetFocus", () => { focusSetter = useSetFocus()(this); private inputRef = createRef>(); - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -132,8 +132,8 @@ describe("useSetFocus", () => { private inputRef = createRef>(); private ready = false; - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -153,8 +153,8 @@ describe("useSetFocus", () => { focusSetter = useSetFocus()(this); private inputRef = createRef>(); - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -188,8 +188,8 @@ describe("useSetFocus", () => { private inputRef = createRef>(); - async setFocus(): Promise { - return this.focusSetter(() => this.inputRef.value); + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); } override render(): JsxNode { @@ -219,10 +219,10 @@ describe("useSetFocus", () => { private focusSetter = useSetFocus()(this); private ref = createRef(); - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return { target: this.ref.value!, includeContainer: true }; - }); + }, options); } override render(): JsxNode { @@ -248,10 +248,10 @@ describe("useSetFocus", () => { private focusSetter = useSetFocus()(this); private ref = createRef(); - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { return this.focusSetter(() => { return { target: this.ref.value!, strategy: "focusable" }; - }); + }, options); } override render(): JsxNode { @@ -273,4 +273,31 @@ describe("useSetFocus", () => { ); }); }); + + it("supports passing focus options", async () => { + class Test extends LitElement { + private focusSetter = useSetFocus()(this); + + inputRef = createRef(); + + async setFocus(options?: FocusOptions): Promise { + return this.focusSetter(() => this.inputRef.value, options); + } + + override render(): JsxNode { + return ; + } + } + + const { el, component } = await mount(Test); + const focusSpy = vi.spyOn(component.inputRef.value!, "focus"); + + expect(document.activeElement).not.toBe(el); + + const focusOptions: FocusOptions = { preventScroll: true }; + await el.setFocus(focusOptions); + expect(document.activeElement).toBe(el); + expect(focusSpy).toHaveBeenCalledWith(focusOptions); + expect(focusSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/calcite-components/src/controllers/useSetFocus.ts b/packages/calcite-components/src/controllers/useSetFocus.ts index f8b0f7a924b..14e5e3e762b 100644 --- a/packages/calcite-components/src/controllers/useSetFocus.ts +++ b/packages/calcite-components/src/controllers/useSetFocus.ts @@ -1,24 +1,19 @@ import { makeGenericController } from "@arcgis/lumina/controllers"; -import { LitElement } from "@arcgis/lumina"; import { componentFocusable } from "../utils/component"; -import { FocusableElement, focusElement, getRootNode } from "../utils/dom"; +import { FocusableElement, focusElement, getRootNode, SetFocusable } from "../utils/dom"; import { type InteractiveComponent } from "../utils/interactive"; type FocusStrategy = "focusable" | "tabbable"; type FocusConfig = { target: FocusableElement; includeContainer?: boolean; strategy?: FocusStrategy }; export interface UseSetFocus { - (getFocusTarget: () => FocusableElement | FocusConfig | undefined): Promise; + (getFocusTarget: () => FocusableElement | FocusConfig | undefined, options?: FocusOptions): Promise; } -interface SetFocusComponent extends LitElement, Partial> { - setFocus: () => Promise; -} +type SetFocusComponent = SetFocusable & Partial>; /** * A controller for centralized setFocus behavior. - * - * @param options */ export const useSetFocus = (): ReturnType< typeof makeGenericController @@ -41,7 +36,7 @@ export const useSetFocus = (): ReturnType< component.el.removeEventListener("focusout", handleFocusOut); }); - return async (getFocusTarget): Promise => { + return async (getFocusTarget, options?: FocusOptions): Promise => { if (component.disabled) { return; } @@ -66,7 +61,7 @@ export const useSetFocus = (): ReturnType< component.el.removeEventListener("focus", handleFocusOut); - return focusElement(target, includeContainer, strategy, component.el); + return focusElement(target, includeContainer, strategy, component.el, options); }; }); }; diff --git a/packages/calcite-components/src/tests/commonTests/focusable.ts b/packages/calcite-components/src/tests/commonTests/focusable.ts index 8b47b2d4ce5..1222dedda33 100644 --- a/packages/calcite-components/src/tests/commonTests/focusable.ts +++ b/packages/calcite-components/src/tests/commonTests/focusable.ts @@ -1,5 +1,6 @@ // @ts-strict-ignore import { expect, it } from "vitest"; +import { GlobalTestProps } from "../utils/puppeteer"; import { getTagAndPage } from "./utils"; import { ComponentTestSetup } from "./interfaces"; @@ -20,7 +21,7 @@ export interface FocusableOptions { * describe("is focusable", () => { * focusable(`calcite-input-number`, { shadowFocusTargetSelector: "input" }) * }); - * @param {string} componentTagOrHTML - the component tag or HTML markup to test against + * * @param componentTestSetup * @param {FocusableOptions} [options] - additional options for asserting focus */ @@ -30,6 +31,7 @@ export function focusable(componentTestSetup: ComponentTestSetup, options?: Focu const element = await page.find(tag); const focusTargetSelector = options?.focusTargetSelector || tag; await element.callMethod("setFocus"); // assumes element is FocusableElement + await page.waitForChanges(); if (options?.shadowFocusTargetSelector) { expect( @@ -41,11 +43,57 @@ export function focusable(componentTestSetup: ComponentTestSetup, options?: Focu ).toBe(true); } - // wait for next frame before checking focus - await page.waitForChanges(); - expect(await page.evaluate((selector) => document.activeElement?.matches(selector), focusTargetSelector)).toBe( true, ); + + // we use a fake to assert that the focus options are passed correctly to the target element + const fakeFocusOptions = { __id__: "fake-focus-options" } as const; + + type TestWindow = GlobalTestProps<{ + receivedFocusOptions: FocusOptions[]; + }>; + + await page.evaluate(() => { + const activeElement = document.activeElement; + + if (activeElement) { + let elementToBlur: Element | null = activeElement; + while (elementToBlur) { + if (elementToBlur.shadowRoot && elementToBlur.shadowRoot.activeElement) { + elementToBlur = elementToBlur.shadowRoot.activeElement; + } else { + (elementToBlur as HTMLElement).blur?.(); + break; + } + } + } + + const originalFocus = HTMLElement.prototype.focus; + HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions) { + const testWindow = window as TestWindow; + testWindow.receivedFocusOptions = testWindow.receivedFocusOptions + ? [...testWindow.receivedFocusOptions, options] + : [options]; + + originalFocus.call(this, options); + }; + }); + await page.waitForChanges(); + + await element.callMethod("setFocus", fakeFocusOptions); + await page.waitForChanges(); + + const receivedFocusOptions = await page.evaluate(() => { + const testWindow = window as TestWindow; + return testWindow.receivedFocusOptions; + }); + + const testScopeFocusOptions = receivedFocusOptions.filter( + (focusOptions) => (focusOptions as typeof fakeFocusOptions)?.__id__ === "fake-focus-options", + ); + + expect(testScopeFocusOptions).toContainEqual(fakeFocusOptions); + expect(testScopeFocusOptions.length).toBe(1); }); } diff --git a/packages/calcite-components/src/tests/commonTests/utils.ts b/packages/calcite-components/src/tests/commonTests/utils.ts index afe1a527843..fc192c3d283 100644 --- a/packages/calcite-components/src/tests/commonTests/utils.ts +++ b/packages/calcite-components/src/tests/commonTests/utils.ts @@ -36,11 +36,8 @@ export function getTag(tagOrHTML: string): ComponentTag { export async function simplePageSetup(componentTagOrHTML: TagOrHTML): Promise { const componentTag = getTag(componentTagOrHTML); - const page = await newE2EPage({ - html: isHTML(componentTagOrHTML) ? componentTagOrHTML : `<${componentTag}>`, - }); - await page.waitForChanges(); - + const page = await newE2EPage(); + await page.setContent(isHTML(componentTagOrHTML) ? componentTagOrHTML : `<${componentTag}>`); return page; } diff --git a/packages/calcite-components/src/utils/component.ts b/packages/calcite-components/src/utils/component.ts index 9c823dd7242..63adf01f5f4 100644 --- a/packages/calcite-components/src/utils/component.ts +++ b/packages/calcite-components/src/utils/component.ts @@ -40,7 +40,7 @@ export function isHidden { + * async focusPart(): Promise { * await componentFocusable(this); * this.internalElement?.focus(); * } diff --git a/packages/calcite-components/src/utils/dom.browser.spec.ts b/packages/calcite-components/src/utils/dom.browser.spec.ts index 3435731de51..63ecabe3184 100644 --- a/packages/calcite-components/src/utils/dom.browser.spec.ts +++ b/packages/calcite-components/src/utils/dom.browser.spec.ts @@ -9,6 +9,7 @@ import { ensureId, focusElement, focusElementInGroup, + focusFirstTabbable, getModeName, getShadowRootNode, getSlotAssignedElements, @@ -611,20 +612,20 @@ describe("dom", () => { this.shadowRoot.innerHTML = `
`; } - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { if (setFocusCalls++ > 10) { // simulates infinite loop without having to trigger a real one in test environment throw new RangeError("setFocus called too many times, likely an infinite loop"); } - return focusElement(this, false, "tabbable", useContext ? this : undefined); + return focusElement(this, false, "tabbable", useContext ? this : undefined, options); } } const testElTag = registerTestElement(Test); const el = document.createElement(testElTag) as Test; - document.body.appendChild(el); + document.body.append(el); vi.spyOn(el, "focus"); vi.spyOn(el, "setFocus"); @@ -641,6 +642,109 @@ describe("dom", () => { expect(error).toBeInstanceOf(RangeError); } }); + + describe("focus options", () => { + it("supports focus options", () => { + const el = create("div", { tabIndex: 0 }); + const focusOptions = { preventScroll: true }; + const focusSpy = vi.spyOn(el, "focus"); + + focusElement(el, true, "tabbable", undefined, focusOptions); + + expect(document.activeElement).toBe(el); + expect(focusSpy).toHaveBeenCalledWith(focusOptions); + expect(focusSpy).toHaveBeenCalledTimes(1); + }); + + it("supports focus options on setFocus elements", () => { + class Test extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = `
`; + } + async setFocus(options?: FocusOptions): Promise { + return focusElement(this, false, "tabbable", this, options); + } + } + const testElTag = registerTestElement(Test); + const el = document.createElement(testElTag) as Test; + document.body.append(el); + vi.spyOn(el, "setFocus"); + + const focusOptions = { preventScroll: true }; + focusElement(el, false, "tabbable", undefined, focusOptions); + + expect(document.activeElement).toBe(el); + expect(el.setFocus).toHaveBeenCalledWith(focusOptions); + expect(el.setFocus).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe("focusFirstTabbable()", () => { + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("focuses the first tabbable element", () => { + const el1 = document.createElement("div"); + const el2 = document.createElement("div"); + el2.tabIndex = 0; + const el3 = document.createElement("div"); + document.body.append(el1, el2, el3); + + focusFirstTabbable(document.body); + + expect(document.activeElement).toBe(el2); + }); + + it("does not focus if no tabbable elements are found", () => { + const el1 = document.createElement("div"); + const el2 = document.createElement("div"); + const el3 = document.createElement("div"); + document.body.append(el1, el2, el3); + + focusFirstTabbable(document.body); + + expect(document.activeElement).toBe(document.body); + }); + + it("supports including parent in focus search", () => { + const el1 = document.createElement("div"); + const el2 = document.createElement("div"); + const el3 = document.createElement("div"); + const container = document.createElement("div"); + el2.tabIndex = 0; + container.tabIndex = 0; + container.append(el1, el2, el3); + document.body.append(container); + + focusFirstTabbable(container); + + expect(document.activeElement).toBe(el2); + + focusFirstTabbable(container, true); + + expect(document.activeElement).toBe(container); + }); + + it("supports passing focus options", () => { + const el1 = document.createElement("div"); + const el2 = document.createElement("div"); + el2.tabIndex = 0; + const el3 = document.createElement("div"); + document.body.append(el1, el2, el3); + + const focusSpy = vi.spyOn(el2, "focus"); + const focusOptions = { preventScroll: true }; + + focusFirstTabbable(document.body, false, focusOptions); + + expect(document.activeElement).toBe(el2); + expect(focusSpy).toHaveBeenCalledWith(focusOptions); + expect(focusSpy).toHaveBeenCalledTimes(1); + }); }); describe("focusElementInGroup()", () => { @@ -713,9 +817,9 @@ describe("dom", () => { this.shadowRoot.innerHTML = `
`; } - async setFocus(): Promise { + async setFocus(options?: FocusOptions): Promise { // simulate setFocus workflow - this.focus(); + this.focus(options); } } @@ -725,7 +829,7 @@ describe("dom", () => { const el = document.createElement(testTag) as Test; el.id = `item-${index}`; el.tabIndex = 0; - document.body.appendChild(el); + document.body.append(el); return el; }); diff --git a/packages/calcite-components/src/utils/dom.ts b/packages/calcite-components/src/utils/dom.ts index d10c9cdee2e..304f11fb411 100644 --- a/packages/calcite-components/src/utils/dom.ts +++ b/packages/calcite-components/src/utils/dom.ts @@ -1,5 +1,6 @@ // @ts-strict-ignore import { focusable, tabbable } from "tabbable"; +import { LitElement } from "@arcgis/lumina"; import { IconNameOrString } from "../components/icon/interfaces"; import { guid } from "./guid"; import { CSS_UTILITY } from "./resources"; @@ -245,19 +246,20 @@ export function containsCrossShadowBoundary(element: Element, maybeDescendant: E return !!walkUpAncestry(maybeDescendant, (node) => (node === element ? true : undefined)); } -/** An element which may contain a `setFocus` method. */ -export interface FocusableElement extends HTMLElement { - setFocus?: () => Promise; +export type FocusableElement = SetFocusable | HTMLElement; + +export interface SetFocusable extends LitElement { + setFocus: (options?: FocusOptions) => Promise; } /** * This helper returns true when an element has a setFocus method. * * @param {Element} el An element. - * @returns {boolean} The result. + * @returns {boolean} Whether the element is focusable. */ -export function isCalciteFocusable(el: FocusableElement): boolean { - return typeof el?.setFocus === "function"; +export function isCalciteFocusable(el: FocusableElement): el is SetFocusable { + return typeof (el as SetFocusable)?.setFocus === "function"; } /** @@ -267,23 +269,27 @@ export function isCalciteFocusable(el: FocusableElement): boolean { * @param includeContainer When true, the container element will be considered as well. Note, this is only applicable when `setFocus` is not applicable. * @param strategy The focus strategy to use when finding the first focusable element. Defaults to "tabbable". * @param context The element invoking the focus – use when the host is focusable to short-circuit the focus call. + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) */ export async function focusElement( el: FocusableElement, includeContainer = false, strategy: "focusable" | "tabbable" = "tabbable", context?: HTMLElement, + options?: FocusOptions, ): Promise { if (!el) { return; } if (isCalciteFocusable(el) && context !== el) { - return el.setFocus(); + return el.setFocus(options); } const firstFocusFunction = strategy === "tabbable" ? focusFirstTabbable : focusFirstFocusable; - return firstFocusFunction(el, includeContainer); + return firstFocusFunction(el, includeContainer, options); } /** @@ -307,9 +313,12 @@ export function getFirstTabbable(element: HTMLElement, includeContainer?: boolea * * @param {HTMLElement} element The html element containing tabbable elements. * @param {boolean} includeContainer When true, the container element will be considered as well. + * @param options - When specified an optional object customizes the component's focusing process. When `preventScroll` is `true`, scrolling will not occur on the component. + * + * @mdn [focus(options)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) */ -export function focusFirstTabbable(element: HTMLElement, includeContainer?: boolean): void { - getFirstTabbable(element, includeContainer)?.focus(); +export function focusFirstTabbable(element: HTMLElement, includeContainer?: boolean, options?: FocusOptions): void { + getFirstTabbable(element, includeContainer)?.focus(options); } /** @@ -338,8 +347,8 @@ function getFirstFocusable(element: HTMLElement, includeContainer?: boolean): HT * * @internal */ -function focusFirstFocusable(element: HTMLElement, includeContainer?: boolean): void { - getFirstFocusable(element, includeContainer)?.focus(); +function focusFirstFocusable(element: HTMLElement, includeContainer?: boolean, options?: FocusOptions): void { + getFirstFocusable(element, includeContainer)?.focus(options); } /**