diff --git a/packages/ai/cypress/specs/TextArea.cy.tsx b/packages/ai/cypress/specs/TextArea.cy.tsx index 4c2bb7915a44..44a0eb7e4c68 100644 --- a/packages/ai/cypress/specs/TextArea.cy.tsx +++ b/packages/ai/cypress/specs/TextArea.cy.tsx @@ -540,7 +540,7 @@ describe("Basic", () => { .find("[ui5-ai-versioning]") .shadow() .find('[data-ui5-versioning-button="previous"]') - .should("have.attr", "tooltip", "Previous Version"); + .should("have.attr", "tooltip", "Previous Version (Ctrl + Shift + Z)"); cy.get("[ui5-ai-textarea]") .shadow() @@ -549,7 +549,83 @@ describe("Basic", () => { .find("[ui5-ai-versioning]") .shadow() .find('[data-ui5-versioning-button="next"]') - .should("have.attr", "tooltip", "Next Version"); + .should("have.attr", "tooltip", "Next Version (Ctrl + Shift + Y)"); + }); + }); + + describe("AI Button Focus Visibility", () => { + it("should show AI button only when textarea, button, or menu has focus", () => { + cy.mount( + + ); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .should("not.exist"); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("textarea") + .focus(); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .should("exist") + .should("be.visible"); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .focus(); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .should("exist") + .should("be.visible"); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .realClick(); + + cy.get("[ui5-ai-textarea]") + .find("ui5-menu") + .should("have.prop", "open", true); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .should("exist") + .should("be.visible"); + + cy.get("body").click(); + + cy.get("[ui5-ai-textarea]") + .shadow() + .find("[ui5-ai-writing-assistant]") + .shadow() + .find("#ai-menu-btn") + .should("not.exist"); }); }); @@ -602,4 +678,4 @@ describe("Basic", () => { .should("not.exist"); }); }); -}); \ No newline at end of file +}); diff --git a/packages/ai/src/TextArea.ts b/packages/ai/src/TextArea.ts index fac2be1b0129..8c1f13118ab2 100644 --- a/packages/ai/src/TextArea.ts +++ b/packages/ai/src/TextArea.ts @@ -83,6 +83,8 @@ class TextArea extends BaseTextArea { // Store bound handler for proper cleanup private _keydownHandler?: (event: KeyboardEvent) => void; + private _menuFocusinHandler?: () => void; + private _menuFocusoutHandler?: (event: Event) => void; /** * Defines whether the `ui5-ai-textarea` is currently in a loading(processing) state. @@ -124,6 +126,9 @@ class TextArea extends BaseTextArea { @property({ type: Number }) totalVersions = 0; + @property({ type: Boolean }) + focused = false; + @slot({ type: HTMLElement }) menu!: Array; @@ -199,12 +204,53 @@ class TextArea extends BaseTextArea { onAfterRendering() { super.onAfterRendering(); - // Add keydown event listener to the textarea const textarea = this.shadowRoot?.querySelector("textarea"); if (textarea && !this._keydownHandler) { this._keydownHandler = this._handleKeydown.bind(this); textarea.addEventListener("keydown", this._keydownHandler); } + + const menuNodes = this.getSlottedNodes("menu"); + if (menuNodes.length > 0) { + const menu = menuNodes[0]; + if (!this._menuFocusinHandler) { + this._menuFocusinHandler = () => { + this.focused = true; + }; + menu.addEventListener("focusin", this._menuFocusinHandler); + } + if (!this._menuFocusoutHandler) { + this._menuFocusoutHandler = (evt: Event) => { + const e = evt as FocusEvent; + const relatedTarget = e.relatedTarget as HTMLElement; + const focusMovingWithinComponent = relatedTarget && this.shadowRoot?.contains(relatedTarget); + const focusStayingInMenu = relatedTarget && menu.contains(relatedTarget); + if (!focusMovingWithinComponent && !focusStayingInMenu) { + this.focused = false; + } + }; + menu.addEventListener("focusout", this._menuFocusoutHandler); + } + } + } + + _onfocusin() { + super._onfocusin(); + this.focused = true; + } + + _onfocusout(e: FocusEvent) { + super._onfocusout(e); + const relatedTarget = e.relatedTarget as HTMLElement; + const focusMovingWithinShadowDOM = relatedTarget && this.shadowRoot?.contains(relatedTarget); + const menuNodes = this.getSlottedNodes("menu"); + const focusMovingToMenu = menuNodes.length > 0 && relatedTarget && ( + menuNodes[0].contains(relatedTarget) + || relatedTarget === menuNodes[0] + ); + if (!focusMovingWithinShadowDOM && !focusMovingToMenu) { + this.focused = false; + } } /** diff --git a/packages/ai/src/TextAreaTemplate.tsx b/packages/ai/src/TextAreaTemplate.tsx index 53764e6dc14d..017bc1470fbd 100644 --- a/packages/ai/src/TextAreaTemplate.tsx +++ b/packages/ai/src/TextAreaTemplate.tsx @@ -58,6 +58,7 @@ export default function TextAreaTemplate(this: TextArea) { currentVersion={this.currentVersion} totalVersions={this.totalVersions} promptDescription={this.promptDescription} + focused={this.focused} onButtonClick={this._handleAIButtonClick} onStopGeneration={this.handleStopGeneration} onVersionChange={this._handleVersionChange} diff --git a/packages/ai/src/WritingAssistant.ts b/packages/ai/src/WritingAssistant.ts index 816238fabab6..400d350ce04b 100644 --- a/packages/ai/src/WritingAssistant.ts +++ b/packages/ai/src/WritingAssistant.ts @@ -152,6 +152,9 @@ class WritingAssistant extends UI5Element { @property({ type: Number }) totalVersions = 0; + @property({ type: Boolean }) + focused = false; + @i18n("@ui5/webcomponents-ai") static i18nBundleAi: I18nBundle; diff --git a/packages/ai/src/WritingAssistantTemplate.tsx b/packages/ai/src/WritingAssistantTemplate.tsx index 32710141222d..94c39c4718fa 100644 --- a/packages/ai/src/WritingAssistantTemplate.tsx +++ b/packages/ai/src/WritingAssistantTemplate.tsx @@ -33,17 +33,19 @@ export default function WritingAssistantTemplate(this: WritingAssistant) { - + {this.focused && ( + + )} ); } diff --git a/packages/ai/src/i18n/messagebundle.properties b/packages/ai/src/i18n/messagebundle.properties index e1b974982bc4..ae28c8e0db39 100644 --- a/packages/ai/src/i18n/messagebundle.properties +++ b/packages/ai/src/i18n/messagebundle.properties @@ -44,7 +44,7 @@ WRITING_ASSISTANT_BUTTON_TOOLTIP=Writing Assistant (Shift + F4) WRITING_ASSISTANT_STOP_TOOLTIP=Stop Generating (Esc) #XFLD: Tooltip for the Previous Version button -VERSIONING_PREVIOUS_BUTTON_TOOLTIP=Previous Version +VERSIONING_PREVIOUS_BUTTON_TOOLTIP=Previous Version (Ctrl + Shift + Z) #XFLD: Tooltip for the Next Version button -VERSIONING_NEXT_BUTTON_TOOLTIP=Next Version +VERSIONING_NEXT_BUTTON_TOOLTIP=Next Version (Ctrl + Shift + Y) diff --git a/packages/ai/src/themes/WritingAssistant.css b/packages/ai/src/themes/WritingAssistant.css index 776c82717d5f..85db0b7a353e 100644 --- a/packages/ai/src/themes/WritingAssistant.css +++ b/packages/ai/src/themes/WritingAssistant.css @@ -27,7 +27,6 @@ border-bottom: none; border-top: none; padding: 0.5rem; - min-height: 2.75rem; display: flex; align-items: center; justify-content: flex-start; diff --git a/packages/ai/test/pages/TextArea.html b/packages/ai/test/pages/TextArea.html index b573e67fe99e..0a28f1047900 100644 --- a/packages/ai/test/pages/TextArea.html +++ b/packages/ai/test/pages/TextArea.html @@ -58,7 +58,7 @@ - +

AI TextArea Component

@@ -125,7 +125,7 @@

AI TextArea Component

{ text: "Regenerate", action: "regenerate", - processingLabel: "Regenerating text...", + processingLabel: "Regenerating ...", completedLabel: "Regenerated text", textKey: "en", replaces: "generate" @@ -237,7 +237,7 @@

AI TextArea Component

if (!hasHistory) { const generateItem = document.createElement('ui5-menu-item'); - generateItem.setAttribute('text', 'Generate text'); + generateItem.setAttribute('text', 'Generate'); generateItem.dataset.action = 'generate'; generateItem.dataset.processingLabel = 'Generating...'; generateItem.dataset.completedLabel = 'Generated text'; diff --git a/packages/website/docs/_samples/ai/TextArea/Basic/sample.html b/packages/website/docs/_samples/ai/TextArea/Basic/sample.html index 691bf3969bab..43fd669d17c7 100644 --- a/packages/website/docs/_samples/ai/TextArea/Basic/sample.html +++ b/packages/website/docs/_samples/ai/TextArea/Basic/sample.html @@ -14,7 +14,7 @@ rows="8" placeholder="Write your content here..."> - + diff --git a/packages/website/docs/_samples/ai/TextArea/Extended/main.js b/packages/website/docs/_samples/ai/TextArea/Extended/main.js index f85217ce2480..fe56b5c3fd33 100644 --- a/packages/website/docs/_samples/ai/TextArea/Extended/main.js +++ b/packages/website/docs/_samples/ai/TextArea/Extended/main.js @@ -28,7 +28,7 @@ const MENU_CONFIG = [ { text: "Regenerate", action: "regenerate", - processingLabel: "Regenerating text...", + processingLabel: "Regenerating ...", completedLabel: "Regenerated text", textKey: "en", replaces: "generate" @@ -141,9 +141,9 @@ function buildMenuFromConfig() { // Add initial "Generate text" option if no history if (!hasHistory) { const generateItem = document.createElement('ui5-menu-item'); - generateItem.setAttribute('text', 'Generate text'); + generateItem.setAttribute('text', 'Generate'); generateItem.dataset.action = 'generate'; - generateItem.dataset.processingLabel = 'Generating text...'; + generateItem.dataset.processingLabel = 'Generating ...'; generateItem.dataset.completedLabel = 'Generated text'; generateItem.dataset.textKey = 'en'; menu.appendChild(generateItem);