Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions packages/ai/cypress/specs/TextArea.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(
<TextArea>
<Menu slot="menu" id="test-menu">
<MenuItem text="Generate text" />
<MenuItem text="Summarize" />
</Menu>
</TextArea>
);

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");
});
});

Expand Down Expand Up @@ -602,4 +678,4 @@ describe("Basic", () => {
.should("not.exist");
});
});
});
});
48 changes: 47 additions & 1 deletion packages/ai/src/TextArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -124,6 +126,9 @@ class TextArea extends BaseTextArea {
@property({ type: Number })
totalVersions = 0;

@property({ type: Boolean })
focused = false;

@slot({ type: HTMLElement })
menu!: Array<HTMLElement>;

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/ai/src/TextAreaTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions packages/ai/src/WritingAssistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
24 changes: 13 additions & 11 deletions packages/ai/src/WritingAssistantTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ export default function WritingAssistantTemplate(this: WritingAssistant) {

<ToolbarSpacer />

<ToolbarButton
id="ai-menu-btn"
design="Transparent"
icon={this.loading ? "stop" : "ai"}
data-state={this.loading ? "generating" : "generate"}
onClick={this.handleButtonClick}
tooltip={this.loading ? this._stopTooltip : this._buttonTooltip}
accessibilityAttributes={{ hasPopup: this.loading ? "false" : "menu" }}
accessibleName={this._buttonAccessibleName}
overflowPriority="NeverOverflow"
/>
{this.focused && (
<ToolbarButton
id="ai-menu-btn"
design="Transparent"
icon={this.loading ? "stop" : "ai"}
data-state={this.loading ? "generating" : "generate"}
onClick={this.handleButtonClick}
tooltip={this.loading ? this._stopTooltip : this._buttonTooltip}
accessibilityAttributes={{ hasPopup: this.loading ? "false" : "menu" }}
accessibleName={this._buttonAccessibleName}
overflowPriority="NeverOverflow"
/>
)}
</Toolbar>
);
}
4 changes: 2 additions & 2 deletions packages/ai/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 0 additions & 1 deletion packages/ai/src/themes/WritingAssistant.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/ai/test/pages/TextArea.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</style>
</head>

<body>
<body class="sapUiSizeCompact">
<div class="demo-container">
<h1 class="demo-title">AI TextArea Component</h1>

Expand Down Expand Up @@ -125,7 +125,7 @@ <h1 class="demo-title">AI TextArea Component</h1>
{
text: "Regenerate",
action: "regenerate",
processingLabel: "Regenerating text...",
processingLabel: "Regenerating ...",
completedLabel: "Regenerated text",
textKey: "en",
replaces: "generate"
Expand Down Expand Up @@ -237,7 +237,7 @@ <h1 class="demo-title">AI TextArea Component</h1>

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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
rows="8" placeholder="Write your content here...">

<ui5-menu slot="menu" id="ai-menu" data-completed-label="Generation complete">
<ui5-menu-item text="Generate text" data-action="generate"></ui5-menu-item>
<ui5-menu-item text="Generate" data-action="generate"></ui5-menu-item>
</ui5-menu>
</ui5-ai-textarea>
<!-- playground-fold -->
Expand Down
6 changes: 3 additions & 3 deletions packages/website/docs/_samples/ai/TextArea/Extended/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const MENU_CONFIG = [
{
text: "Regenerate",
action: "regenerate",
processingLabel: "Regenerating text...",
processingLabel: "Regenerating ...",
completedLabel: "Regenerated text",
textKey: "en",
replaces: "generate"
Expand Down Expand Up @@ -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);
Expand Down
Loading