From 08e1dde3dd2e0cb89d3be5c1312d29f3be3f8c74 Mon Sep 17 00:00:00 2001 From: Nikolay Deshev Date: Fri, 5 Dec 2025 12:59:29 +0200 Subject: [PATCH 1/3] fix(ui5-ai-textarea): add docs and correct samples adjust samples and test pages to reflect the changes made in #12638 --- packages/ai/test/pages/TextArea.html | 29 +++++++++++--- .../_samples/ai/TextArea/Extended/main.js | 39 ++++++++++++------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/ai/test/pages/TextArea.html b/packages/ai/test/pages/TextArea.html index 0a93922e8a7c..5ab08eeb80ec 100644 --- a/packages/ai/test/pages/TextArea.html +++ b/packages/ai/test/pages/TextArea.html @@ -193,7 +193,7 @@

AI TextArea Component

textarea.value = versionHistory[versionIndex].value; } - textarea.currentVersion = currentIndexHistory; + textarea.currentVersion = currentIndexHistory + 1; textarea.totalVersions = versionHistory.length; if (versionHistory[currentIndexHistory]) { @@ -366,13 +366,30 @@

AI TextArea Component

stopTypingAnimation(); currentGenerationIndex += 1; - // Restore the previous content instead of saving the cancelled action - textarea.value = contentBeforeGeneration; + // Save the stopped generation if there is content + const stoppedValue = textarea.value; + if (stoppedValue.trim()) { + const menuItem = findMenuItemByAction(currentActionInProgress); + const completedLabel = (menuItem && menuItem.dataset.completedLabel) + ? menuItem.dataset.completedLabel + " (stopped)" + : "Generation stopped"; + + versionHistory.push({ + value: stoppedValue, + action: currentActionInProgress, + endAction: completedLabel, + timestamp: new Date().toISOString() + }); + + // Restore the previous content + textarea.value = contentBeforeGeneration; + currentIndexHistory = versionHistory.length - 1; + buildMenuFromConfig(); + updateComponentState(); + } + currentActionInProgress = null; textarea.loading = false; - textarea.promptDescription = ""; - - updateComponentState(); } function handleVersionChange(event) { diff --git a/packages/website/docs/_samples/ai/TextArea/Extended/main.js b/packages/website/docs/_samples/ai/TextArea/Extended/main.js index 755e2eadb5fe..262d57e30f45 100644 --- a/packages/website/docs/_samples/ai/TextArea/Extended/main.js +++ b/packages/website/docs/_samples/ai/TextArea/Extended/main.js @@ -72,6 +72,7 @@ let currentIndexHistory = 0; let currentActionInProgress = null; let typingInterval = null; let currentGenerationIndex = 0; +let contentBeforeGeneration = ""; const textarea = document.getElementById('ai-textarea'); const menu = document.getElementById('ai-menu'); @@ -95,7 +96,7 @@ function updateComponentState(versionIndex = null) { textarea.value = versionHistory[versionIndex].value; } - textarea.currentVersion = currentIndexHistory; + textarea.currentVersion = currentIndexHistory + 1; textarea.totalVersions = versionHistory.length; if (versionHistory[currentIndexHistory]) { @@ -242,6 +243,7 @@ async function executeAction(action) { const textKey = menuItem.dataset.textKey || 'en'; saveCurrentVersion(); + contentBeforeGeneration = textarea.value; currentActionInProgress = action; currentGenerationIndex += 1; const generationIdForThisRun = currentGenerationIndex; @@ -267,22 +269,29 @@ function stopGeneration() { stopTypingAnimation(); currentGenerationIndex += 1; - const action = currentActionInProgress || 'generate'; - const menuItem = findMenuItemByAction(action); - const completedLabel = (menuItem && menuItem.dataset.completedLabel) ? menuItem.dataset.completedLabel : 'Action completed'; - - versionHistory.push({ - value: textarea.value, - action, - endAction: completedLabel + " (stopped)", - timestamp: new Date().toISOString() - }); + + // Save the stopped generation if there is content + const stoppedValue = textarea.value; + if (stoppedValue.trim()) { + const action = currentActionInProgress || 'generate'; + const menuItem = findMenuItemByAction(action); + const completedLabel = (menuItem && menuItem.dataset.completedLabel) ? menuItem.dataset.completedLabel : 'Action completed'; + + versionHistory.push({ + value: stoppedValue, + action, + endAction: completedLabel + " (stopped)", + timestamp: new Date().toISOString() + }); + + // Restore the previous content + textarea.value = contentBeforeGeneration; + currentIndexHistory = versionHistory.length - 1; + buildMenuFromConfig(); + updateComponentState(); + } - currentIndexHistory = versionHistory.length - 1; currentActionInProgress = null; - - buildMenuFromConfig(); - updateComponentState(); textarea.loading = false; } From 203d9e65a0d517efae07d7522c8dcbd96c2b31b6 Mon Sep 17 00:00:00 2001 From: Nikolay Deshev Date: Wed, 17 Dec 2025 09:50:43 +0200 Subject: [PATCH 2/3] docs(ui5-ai-textarea): add docs and correct samples do not restore previous value on generation cancelation --- packages/ai/test/pages/TextArea.html | 3 --- packages/website/docs/_samples/ai/TextArea/Extended/main.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/ai/test/pages/TextArea.html b/packages/ai/test/pages/TextArea.html index 5ab08eeb80ec..b573e67fe99e 100644 --- a/packages/ai/test/pages/TextArea.html +++ b/packages/ai/test/pages/TextArea.html @@ -366,7 +366,6 @@

AI TextArea Component

stopTypingAnimation(); currentGenerationIndex += 1; - // Save the stopped generation if there is content const stoppedValue = textarea.value; if (stoppedValue.trim()) { const menuItem = findMenuItemByAction(currentActionInProgress); @@ -381,8 +380,6 @@

AI TextArea Component

timestamp: new Date().toISOString() }); - // Restore the previous content - textarea.value = contentBeforeGeneration; currentIndexHistory = versionHistory.length - 1; buildMenuFromConfig(); updateComponentState(); diff --git a/packages/website/docs/_samples/ai/TextArea/Extended/main.js b/packages/website/docs/_samples/ai/TextArea/Extended/main.js index 262d57e30f45..f85217ce2480 100644 --- a/packages/website/docs/_samples/ai/TextArea/Extended/main.js +++ b/packages/website/docs/_samples/ai/TextArea/Extended/main.js @@ -270,7 +270,6 @@ function stopGeneration() { stopTypingAnimation(); currentGenerationIndex += 1; - // Save the stopped generation if there is content const stoppedValue = textarea.value; if (stoppedValue.trim()) { const action = currentActionInProgress || 'generate'; @@ -284,8 +283,6 @@ function stopGeneration() { timestamp: new Date().toISOString() }); - // Restore the previous content - textarea.value = contentBeforeGeneration; currentIndexHistory = versionHistory.length - 1; buildMenuFromConfig(); updateComponentState(); From 00f36ad8a628046234fac6897b19e44b1fd8d8d2 Mon Sep 17 00:00:00 2001 From: Nikolay Deshev Date: Wed, 17 Dec 2025 19:39:34 +0200 Subject: [PATCH 3/3] feat(ai-textarea): apply UX feedback adjustments JIRA: BGSOFUIRILA-4033 --- packages/ai/cypress/specs/TextArea.cy.tsx | 82 ++++++++++++++++++- packages/ai/src/TextArea.ts | 48 ++++++++++- packages/ai/src/TextAreaTemplate.tsx | 1 + packages/ai/src/WritingAssistant.ts | 3 + packages/ai/src/WritingAssistantTemplate.tsx | 24 +++--- packages/ai/src/i18n/messagebundle.properties | 4 +- packages/ai/src/themes/WritingAssistant.css | 1 - packages/ai/test/pages/TextArea.html | 6 +- .../_samples/ai/TextArea/Basic/sample.html | 2 +- .../_samples/ai/TextArea/Extended/main.js | 6 +- 10 files changed, 152 insertions(+), 25 deletions(-) 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);