diff --git a/packages/ai/cypress/specs/Input.cy.tsx b/packages/ai/cypress/specs/Input.cy.tsx new file mode 100644 index 000000000000..48b4325ad1f5 --- /dev/null +++ b/packages/ai/cypress/specs/Input.cy.tsx @@ -0,0 +1,839 @@ +import AIInput from "../../src/Input.js"; +import MenuItem from "@ui5/webcomponents/dist/MenuItem.js"; +import "@ui5/webcomponents-icons/dist/ai.js"; +import "@ui5/webcomponents-icons/dist/stop.js"; +import { VERSIONING_NEXT_BUTTON_TOOLTIP, VERSIONING_PREVIOUS_BUTTON_TOOLTIP, INPUT_WRITING_ASSISTANT_LABEL } from "../../src/generated/i18n/i18n-defaults.js"; +import Input from "../../src/Input.js"; + +describe("Basic", () => { + describe("Initialization", () => { + it("should render with default properties", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .as("input") + .should("exist") + .should("have.prop", "loading", false) + .should("have.prop", "currentVersion", 0) + .should("have.prop", "totalVersions", 0); + }); + + it("should set initial value as a property", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .should("have.prop", "value", "AI initial value"); + }); + }); + + describe("Loading States", () => { + it("should display non-loading state correctly", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-busy-indicator]") + .should("not.have.attr", "active"); + }); + + it("should display loading state correctly", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-busy-indicator]") + .should("have.attr", "active"); + }); + + it("should display single result correctly", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .should("have.prop", "loading", false) + .should("have.prop", "value", "Generated text") + .should("have.prop", "currentVersion", 1) + .should("have.prop", "totalVersions", 1); + }); + + it("should display multiple results correctly", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .should("have.prop", "loading", false) + .should("have.prop", "value", "Generated text") + .should("have.prop", "currentVersion", 2) + .should("have.prop", "totalVersions", 3); + }); + }); + + describe("Version Navigation", () => { + it("should fire version-change event with backwards=true for previous version", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[ui5-menu]') + .as("menu"); + + cy.get("@menu") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .realClick(); + + cy.get("@onVersionChange") + .should("have.been.calledOnce") + .its("firstCall.args.0.detail") + .should("deep.equal", { + backwards: true + }); + }); + + it("should fire version-change event with backwards=false for next version", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[ui5-menu]') + .as("menu"); + + cy.get("@menu") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .realClick(); + + cy.get("@onVersionChange") + .should("have.been.calledOnce") + .its("firstCall.args.0.detail") + .should("deep.equal", { + backwards: false, + }); + }); + + it("should disable previous button when at first version", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .should("have.attr", "disabled"); + }); + + it("should disable next button when at last version", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "disabled"); + }); + + it("should sync input content after version navigation", () => { + const initialValue = "Version 1 content"; + const newValue = "Version 2 content"; + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .as("input") + .then($input => { + $input[0].addEventListener("version-change", () => { + const input = $input[0] as Input; + input.value = newValue; + }); + }); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[ui5-menu]') + .as("menu"); + + cy.get("@menu") + .ui5MenuOpen(); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .realClick(); + + cy.get("@onVersionChange") + .should("have.been.calledOnce"); + + cy.get("@input") + .shadow() + .find("input") + .should("have.value", newValue); + }); + }); + + describe("Menu Integration", () => { + it("should open menu when AI Icon is clicked", () => { + cy.mount( + + + + ); + + cy.get("[ui5-ai-input]") + .realClick(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("[ui5-ai-input") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpened(); + }); + }); + + describe("Stop Generation", () => { + it("should fire stop-generation event", () => { + const onStopGeneration = cy.spy().as("onStopGeneration"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("@onStopGeneration").should("have.been.calledOnce"); + }); + }); + + describe("Keyboard Shortcuts", () => { + it("should handle Shift+F4 to open menu", () => { + cy.mount( + + + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus() + .realPress(['Shift', 'F4']); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpened(); + }); + + it("should handle Ctrl+Shift+Z for previous version when multiple versions exist", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus() + .realPress(['Control', 'Shift', 'Z']); + + cy.get("@onVersionChange") + .should("have.been.calledOnce") + .its("firstCall.args.0.detail") + .should("deep.equal", { + backwards: true + }); + }); + + it("should handle Ctrl+Shift+Y for next version when multiple versions exist", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus() + .realPress(['Control', 'Shift', 'Y']); + + cy.get("@onVersionChange") + .should("have.been.calledOnce") + .its("firstCall.args.0.detail") + .should("deep.equal", { + backwards: false + }); + }); + }); + + describe("Busy State", () => { + it("should show busy indicator when loading", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-busy-indicator]") + .should("have.attr", "active"); + }); + + it("should hide busy indicator when not loading", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("ui5-busy-indicator") + .should("not.have.attr", "active"); + }); + }); +}); + +describe("Versioning Menu Item", () => { + describe("Initialization", () => { + it("should render with current step and total steps as text", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input") + .shadow() + .find("[ui5-menu-item]") + .as("menuItemVersioning"); + cy.get("@menuItemVersioning") + .should("exist") + .should("have.attr", "text", "2 / 3"); + }); + }); + + describe("Navigation Buttons", () => { + it("should enable both buttons when in middle steps", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input") + .shadow() + .find("[ui5-menu-item]") + .as("menuItemVersioning"); + + cy.get("@menuItemVersioning") + .find('[data-ui5-versioning-button="previous"]') + .should("not.have.attr", "disabled"); + + cy.get("@menuItemVersioning") + .find('[data-ui5-versioning-button="next"]') + .should("not.have.attr", "disabled"); + }); + + it("should have proper icons", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input") + .shadow() + .find("[ui5-menu-item]") + .as("menuItemVersioning"); + + cy.get("@menuItemVersioning") + .find('[data-ui5-versioning-button="previous"]') + .should("have.attr", "icon", "navigation-left-arrow"); + + cy.get("@menuItemVersioning") + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "icon", "navigation-right-arrow"); + }); + + it("should not fire events when buttons are disabled", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .as("previousButton") + .should("have.attr", "disabled"); + + cy.get("@previousButton") + .realClick(); + + cy.get("@onVersionChange").should("not.have.been.called"); + }); + + it("should handle multiple rapid clicks gracefully", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .realClick() + .realClick() + .realClick(); + + cy.get("@onVersionChange").should("have.callCount", 3); + + // Verify all calls were for next (backwards: false) + cy.get("@onVersionChange").should((spy) => { + expect(spy).to.have.been.calledWith(Cypress.sinon.match.has("detail", { backwards: false })); + }); + }); + }) + + describe("Focus Management", () => { + it("should manage focus when reaching boundaries", () => { + const onVersionChange = cy.spy().as("onVersionChange"); + + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .as("input"); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + // Test that buttons respond correctly when reaching boundaries + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .as("nextButton") + .should("not.have.attr", "disabled"); + + cy.get("@nextButton").realClick(); + + cy.get("@onVersionChange").should("have.been.calledOnce"); + + // Simulate reaching the last step - next button should be disabled + cy.get("@input").invoke("prop", "currentVersion", 3); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "disabled"); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .as("previousButton") + .should("not.have.attr", "disabled"); + + cy.get("@previousButton").realClick(); + + cy.get("@onVersionChange").should("have.been.calledTwice"); + + // Simulate reaching the first step - previous button should be disabled + cy.get("@input").invoke("prop", "currentVersion", 1); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .should("have.attr", "disabled"); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("not.have.attr", "disabled"); + }); + + it("should not change focus when buttons remain enabled", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .as("input"); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .realClick(); + + // Simulate property change without reaching boundary + cy.get("@input").invoke("prop", "currentVersion", 4); + + // The button should still exist and be enabled + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .should("not.have.attr", "disabled"); + }); + }); + + describe("Step Display", () => { + it("should update display when properties change", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .as("input") + .invoke("prop", "currentVersion", 2) + .invoke("prop", "totalVersions", 4); + + cy.get("@input") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("@input") + .shadow() + .find("[ui5-menu-item]") + .should("have.attr", "text", "2 / 4"); + }); + + it("should handle large numbers correctly", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-menu-item]") + .should("have.attr", "text", "999 / 1000"); + }); + }); + + describe("Button State Transitions", () => { + it("should handle rapid property changes", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .as("input"); + + // Rapidly change properties + for (let i = 1; i <= 5; i++) { + cy.get("@input").invoke("prop", "currentVersion", i); + } + + cy.get("@input") + .shadow() + .find("[ui5-menu]") + .ui5MenuOpen(); + + cy.get("@input") + .shadow() + .find("[ui5-menu-item]") + .should("have.attr", "text", "5 / 5"); + + cy.get("@input") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "disabled"); + }); + }); + + describe("Accessibility", () => { + it("should support keyboard navigation", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .realClick() + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + // Test that buttons are clickable (simulating keyboard activation) + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .as("previousButton"); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .as("nextButton"); + + cy.realPress("ArrowDown") + .realPress("ArrowRight") + .realPress("Enter"); + + cy.get("@onVersionChange").should("have.been.calledOnce"); + + cy.realPress("ArrowRight"); + cy.realPress("Enter"); + + cy.get("@onVersionChange").should("have.been.calledTwice"); + }); + + it("should have proper ARIA attributes", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .should("have.attr", "design", "Transparent"); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "design", "Transparent"); + }); + + + it("should have translatable previous button tooltip", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="previous"]') + .should("have.attr", "tooltip", VERSIONING_PREVIOUS_BUTTON_TOOLTIP.defaultText); + }); + + it("should have translatable next button tooltip", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find('[data-ui5-versioning-button="next"]') + .should("have.attr", "tooltip", VERSIONING_NEXT_BUTTON_TOOLTIP.defaultText); + }); + }); +}); + +describe("Writing Assistant Input Icon", () => { + it("should not be visible when input is not focused", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .should("not.be.visible"); + }) + it("should render AI Icon on focus", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .should("be.visible") + .and("have.prop", "name", "ai"); + }); + it("should show generating state when loading", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .should("have.prop", "name", "stop"); + }); + it("should fire icon-click event when clicked in non-loading state", () => { + cy.mount( + + ); + + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("@onButtonClick") + .should("have.been.calledOnce"); + }); + + it("should fire stop-generation event when clicked in loading state", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-input]") + .shadow() + .find("input") + .focus(); + + cy.get("[ui5-ai-input]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("@onStopGeneration").should("have.been.calledOnce"); + }); + + it("should have proper ariaKeyShortcuts accessibility attribute", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find(".ui5-input-ai-icon") + .should("have.attr", "aria-keyshortcuts", "Shift + F4"); + }); + + it("should have correct aria-label attribute", () => { + cy.mount(); + + cy.get("[ui5-ai-input]") + .shadow() + .find(".ui5-input-ai-icon") + .should("have.attr", "aria-label", INPUT_WRITING_ASSISTANT_LABEL.defaultText); + }); +}) + diff --git a/packages/ai/src/Input.ts b/packages/ai/src/Input.ts new file mode 100644 index 000000000000..bd9482ff7450 --- /dev/null +++ b/packages/ai/src/Input.ts @@ -0,0 +1,259 @@ +import { customElement, property, slot } from "@ui5/webcomponents-base"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import BaseInput from "@ui5/webcomponents/dist/Input.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; +import type Menu from "@ui5/webcomponents/dist/Menu.js"; +import type Button from "./Button.js"; + +// styles +import AIInputCss from "./generated/themes/Input.css.js"; +import InputCss from "@ui5/webcomponents/dist/generated/themes/Input.css.js"; +import ResponsivePopoverCommonCss from "@ui5/webcomponents/dist/generated/themes/ResponsivePopoverCommon.css.js"; +import ValueStateMessageCss from "@ui5/webcomponents/dist/generated/themes/ValueStateMessage.css.js"; +import SuggestionsCss from "@ui5/webcomponents/dist/generated/themes/Suggestions.css.js"; + +// templates +import InputTemplate from "./InputTemplate.js"; +import { + VERSIONING_NEXT_BUTTON_TEXT, + VERSIONING_PREVIOUS_BUTTON_TEXT, + INPUT_WRITING_ASSISTANT_LABEL, + WRITING_ASSISTANT_GENERATING_ANNOUNCEMENT, +} from "./generated/i18n/i18n-defaults.js"; + +@customElement({ + tag: "ui5-ai-input", + languageAware: true, + renderer: jsxRenderer, + template: InputTemplate, + styles: [ + AIInputCss, + InputCss, + ResponsivePopoverCommonCss, + ValueStateMessageCss, + SuggestionsCss, + ], +}) + +/** + * Fired when the user clicks on the AI button. + * @public + */ +@event("button-click", { + cancelable: true, +}) + +/** + * Fired when the user clicks on the "Stop" button to stop ongoing AI text generation. + * @public + */ +@event("stop-generation") + +/** + * Fired when the user clicks on version navigation buttons. + * + * @param {boolean} backwards - Indicates if navigation is backwards (true) or forwards (false, default) + * @public + */ +@event("version-change") + +class Input extends BaseInput { + eventDetails!: BaseInput["eventDetails"] & { + "version-change": { + backwards: boolean; + }; + "stop-generation": object; + "button-click": object; + }; + + /** + * Indicates the index of the currently displayed version. + * + * @default 0 + */ + @property({ type: Number }) + currentVersion = 0; + + /** + * Indicates the total number of result versions available. + * + * When not set or `0`, versioning UI will be hidden. + * + * @default 0 + * @public + */ + @property({ type: Number }) + totalVersions = 0; + + /** + * Defines whether the AI Writing Assistant is currently loading. + * + * When `true`, indicates that an AI action is in progress. + * + * @default false + */ + @property({ type: Boolean }) + loading: boolean = false; + + /** + * Indicates if the menu is open. + * @default 0 + * @private + */ + @property({ type: Boolean }) + _isMenuOpen: boolean = false; + + /** + * Defines the items of the menu for the component. + * @public + */ + @slot({ + type: HTMLElement, + invalidateOnChildChange: true, + }) + actions!: Array; + + _previousCurrentStep = 0; + _previousTotalSteps = 0; + isFocused: boolean = false; + + _onfocusin(e: FocusEvent): void { + super._onfocusin(e); + this.isFocused = true; + } + + _onfocusout(e: FocusEvent): void { + super._onfocusout(e); + this.isFocused = false; + } + + /** + * Manages focus when navigation buttons become disabled/enabled. + * Automatically moves focus to available button when user reaches boundaries. + * @private + */ + _manageVersionButtonsFocus() { + const previousButton = this.shadowRoot?.querySelectorAll("ui5-button")[0] as Button; + const nextButton = this.shadowRoot?.querySelectorAll("ui5-button")[1] as Button; + const isPreviousDisabled = this.currentVersion <= 1; + const isNextDisabled = this.currentVersion >= this.totalVersions; + + if (isPreviousDisabled && previousButton) { + setTimeout(() => { + nextButton.focus(); + }, 0); + } else if (isNextDisabled && nextButton) { + setTimeout(() => { + previousButton.focus(); + }, 0); + } + } + + /** + * Handles the click event for the AI generate icon. + * Fires the appropriate event based on the AI icon state. + * @private + */ + _handleAIIconClick(e: Event) { + const target = e.target as HTMLElement & { name?: string }; + if (target?.name === "stop") { + this.fireDecoratorEvent("stop-generation"); + } else { + const opener = this.shadowRoot?.querySelector(".ui5-input-ai-icon") as HTMLElement; + this.fireDecoratorEvent("button-click"); + this.menu.opener = opener; + this.menu.open = true; + this.menu.horizontalAlign = "End"; + } + } + + /** + * Handles the version change event from the versioning component. + * + * @param {CustomEvent} e - The version change event + */ + _handleVersionChange(e: CustomEvent<{ backwards: boolean }>) { + this.fireDecoratorEvent("version-change", { + backwards: e.detail.backwards, + }); + this._manageVersionButtonsFocus(); + } + + /** + * Handles the click event for the "Previous Version" button. + * Updates the current version index and syncs content. + * @private + */ + _handlePreviousButtonClick(): void { + this._handleVersionChange(new CustomEvent("version-change", { detail: { backwards: true } })); + } + + /** + * Handles the click event for the "Next Version" button. + * Updates the current version index and syncs content. + * @private + */ + _handleNextButtonClick(): void { + this._handleVersionChange(new CustomEvent("version-change", { detail: { backwards: false } })); + } + + _onMenuIconClick(): void { + this.menu?.addEventListener("item-click", (e: Event) => { + const customEvent = e as CustomEvent; + this.dispatchEvent(new CustomEvent("item-click", { + detail: customEvent.detail, + bubbles: true, + composed: true, + })); + }); + } + + /** + * Handles keydown events for keyboard shortcuts. + * @private + */ + _onkeydown(e: KeyboardEvent): void { + super._onkeydown(e); + this.menu.opener = this.shadowRoot?.querySelector(".ui5-input-ai-icon") as HTMLElement; + + if (e.key === "F4" && e.shiftKey) { + e.preventDefault(); + this.menu.open = true; + this.menu.horizontalAlign = "End"; + } + const goPreviousStep = e.key === "Z" && e.shiftKey && e.ctrlKey; + const goNextStep = e.key === "Y" && e.shiftKey && e.ctrlKey; + + if (goPreviousStep) { + e.preventDefault(); + this._handlePreviousButtonClick(); + } else if (goNextStep) { + e.preventDefault(); + this._handleNextButtonClick(); + } + } + + get ariaLabel() { + return this.accessibleName || !this.loading ? Input.i18nBundle.getText(INPUT_WRITING_ASSISTANT_LABEL) : Input.i18nBundle.getText(WRITING_ASSISTANT_GENERATING_ANNOUNCEMENT); + } + + get stopGeneratingTooltip() { + return Input.i18nBundle.getText(WRITING_ASSISTANT_GENERATING_ANNOUNCEMENT); + } + + get nextButtonAccessibleName() { + return Input.i18nBundle.getText(VERSIONING_NEXT_BUTTON_TEXT); + } + + get previousButtonAccessibleName() { + return Input.i18nBundle.getText(VERSIONING_PREVIOUS_BUTTON_TEXT); + } + + get menu() { + return this.shadowRoot?.querySelector("ui5-menu") as Menu; + } +} + +Input.define(); + +export default Input; diff --git a/packages/ai/src/InputTemplate.tsx b/packages/ai/src/InputTemplate.tsx new file mode 100644 index 000000000000..228af33512bf --- /dev/null +++ b/packages/ai/src/InputTemplate.tsx @@ -0,0 +1,207 @@ +import type Input from "./Input.js"; +import Icon from "@ui5/webcomponents/dist/Icon.js"; +import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js"; +import MenuItem from "@ui5/webcomponents/dist/MenuItem.js"; +import Button from "@ui5/webcomponents/dist/Button.js"; +import Menu from "@ui5/webcomponents/dist/Menu.js"; +import "@ui5/webcomponents-icons/dist/navigation-left-arrow.js"; +import "@ui5/webcomponents-icons/dist/navigation-right-arrow.js"; +import InputPopoverTemplate from "@ui5/webcomponents/dist/InputPopoverTemplate.js"; +import type { JsxTemplateResult } from "@ui5/webcomponents-base"; + +type TemplateHook = () => JsxTemplateResult; + +export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook }) { + const suggestionsList = hooks?.suggestionsList; + const preContent = hooks?.preContent || defaultPreContent; + const postContent = hooks?.postContent || defaultPostContent; + return ( + <> +
+
+ + +
+
+ {preContent.call(this)} + + + + {this._effectiveShowClearIcon && +
+ + +
+ } + + {this.icon.length > 0 && +
+ +
+ } + +
+ {this._valueStateInputIcon} +
+ { postContent.call(this) } + {this._effectiveShowSuggestions && + <> + {this.suggestionsText} + + {this.availableSuggestionsCount} + + } + + {this.accInfo.ariaDescription && + {this.accInfo.ariaDescription} + } + + {this.accInfo.accessibleDescription && + {this.accInfo.accessibleDescription} + } + + {this.linksInAriaValueStateHiddenText.length > 0 && + {this.valueStateLinksShortcutsTextAcc} + } + + {this.hasValueState && + {this.ariaValueStateHiddenText} + } +
+
+ +
+ + { this._isMenuOpen = true; }} + onBeforeClose={() => { this._isMenuOpen = false; }} + > + + {this.totalVersions > 1 && Versioning.call(this)} + +
+
+ {InputPopoverTemplate.call(this, { suggestionsList })} + + ); +} + +function Versioning(this: Input) { + return ( + <> + + +