Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: AI Button Animation #10424

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
225 changes: 138 additions & 87 deletions packages/ai/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import query from "@ui5/webcomponents-base/dist/decorators/query.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import MainButton from "@ui5/webcomponents/dist/Button.js";
import SplitButton from "@ui5/webcomponents/dist/SplitButton.js";
import Icon from "@ui5/webcomponents/dist/Icon.js";
import type ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
import ButtonState from "./ButtonState.js";
Expand All @@ -20,18 +21,21 @@ import ButtonCss from "./generated/themes/Button.css.js";
*
* ### Overview
*
* The `ui5-ai-button` component represents a button used in AI-related scenarios.
* It enables users to trigger actions by clicking or tapping the `ui5-ai-button`, or by pressing
* certain keyboard keys, such as Enter.
* The `ui5-ai-button` component serves as a button for AI-related scenarios. Users can trigger actions by clicking or tapping the `ui5-ai-button`
* or by pressing keyboard keys like [Enter] or [Space].
*
* ### Usage
*
* For the `ui5-ai-button` UI, you can define one or more states of the button by placing `ai-button-state` components in its default slot.
* Each state have a name that identifies it and can have text, icon and end icon defined (in any combination) depending on the state purpose.
* For the `ui5-ai-button` user interface, you can define one or more button states by placing `ui5-ai-button-state` components in their default slot.
* Each state has a name for identification and can include text, an icon, and an end icon, as needed for its purpose.
* You can define a split mode for the `ui5-ai-button`, which will results in displaying an arrow button for additional actions.
*
* You can choose from a set of predefined designs (the same as for regular `ui5-button` component) that allow different styling to correspond to the triggered action.
* You can choose from a set of predefined designs for `ui5-ai-button` (as in `ui5-button`) to match the desired styling.
*
* `ui5-ai-button` can be activated by clicking or tapping it. The state can be changed in `click` event handler.
* The `ui5-ai-button` can be activated by clicking or tapping it. You can change the button state in the click event handler. When the button is
* in split mode, you can activate the default button action by clicking or tapping it, or by pressing keyboard keys like [Enter] or [Space].
* You can activate the arrow button by clicking or tapping it, or by pressing keyboard keys like [Arrow Up], [Arrow Down], or [F4].
* To display additional actions, you can attach a menu to the arrow button.
*
* ### ES6 Module Import
*
Expand All @@ -50,7 +54,7 @@ import ButtonCss from "./generated/themes/Button.css.js";
renderer: jsxRenderer,
template: ButtonTemplate,
styles: ButtonCss,
dependencies: [MainButton, Icon, ButtonState],
dependencies: [SplitButton, Icon, ButtonState],
shadowRootOptions: { delegatesFocus: true },
})

Expand All @@ -62,9 +66,21 @@ import ButtonCss from "./generated/themes/Button.css.js";
@event("click", {
bubbles: true,
})

/**
* Fired when the component is in split mode and after the arrow button
* is activated either by clicking or tapping it or by using the [Arrow Up] / [Arrow Down],
* [Alt] + [Arrow Up]/ [Arrow Down], or [F4] keyboard keys.
* @public
*/
@event("arrow-click", {
bubbles: true,
})

class Button extends UI5Element {
eventDetails!: {
click: void,
"click": void;
"arrow-click": void;
}
/**
* Defines the component design.
Expand Down Expand Up @@ -94,47 +110,85 @@ class Button extends UI5Element {
state?: string;

/**
* Keeps the current state object of the component.
* @private
*/
@property({ type: Object })
_currentStateObject?: ButtonState;

/**
* Initiates button elements fade-out phase.
* Defines the active state of the arrow button in split mode.
* Set to true when the button is in split mode and a menu with additional options
* is opened by the arrow button. Set back to false when the menu is closed.
* @default false
* @private
* @public
* @since 2.6.0
*/
@property({ type: Boolean })
fadeOut = false;
@property({ type: Boolean, noAttribute: true })
activeArrowButton = false;

/**
* Initiates button fade middle phase.
* @default false
* Keeps the current state object of the component.
* @private
*/
@property({ type: Boolean })
fadeMid = false;
@property({ type: Object })
_currentStateObject?: ButtonState;

/**
* Initiates button elements fade-in phase.
* Determines if the button is in icon-only mode.
* This property is animation related only.
* @default false
* @private
*/
@property({ type: Boolean })
fadeIn = false;
iconOnly? = false;

/**
* Defines the available states of the component.
* **Note:** Although this slot accepts HTML Elements, it is strongly recommended that you only use `ui5-ai-button-state` components in order to preserve the intended design.
* **Note:** Although this slot accepts HTML Elements, it is strongly recommended that
* you only use `ui5-ai-button-state` components in order to preserve the intended design.
* @public
*/
@slot({ type: HTMLElement, "default": true })
states!: Array<ButtonState>;

@query("[ui5-split-button]")
_splitButton?: SplitButton;

@query(".ui5-ai-button-hidden[ui5-split-button]")
_hiddenSplitButton?: SplitButton;

get _hideArrowButton() {
return !this._effectiveStateObject?.splitMode;
}

get _effectiveState() {
return this.state || (this.states.length && this.states[0].name) || "";
}

get _effectiveStateObject() {
return this.states.find(state => state.name === this._effectiveState);
}

get _stateIconOnly() {
return !this._stateText && !!this._stateIcon;
}

get _stateText() {
return this._currentStateObject?.text;
}

get _stateIcon() {
return this._currentStateObject?.icon;
}

get _stateEndIcon() {
const endIcon = this._effectiveStateObject?.splitMode ? "" : this._effectiveStateObject?.endIcon;
return endIcon;
}

get _hasText() {
return !!this._stateText;
}

onBeforeRendering(): void {
if (this.fadeOut || this.fadeIn) {
return;
const splitButton = this._splitButton;

if (splitButton) {
splitButton.activeArrowButton = this.activeArrowButton;
}

if (!this._currentStateObject?.name) {
Expand All @@ -143,6 +197,7 @@ class Button extends UI5Element {

const currentStateName = this._currentStateObject?.name || "";

this.iconOnly = this._stateIconOnly;
if (currentStateName !== "" && currentStateName !== this._effectiveState) {
this._fadeOut();
}
Expand All @@ -154,43 +209,59 @@ class Button extends UI5Element {
*/
async _fadeOut(): Promise<void> {
const fadeOutDuration = 180;

const button = this._mainButton;
const button = this._splitButton;
const hiddenButton = this._hiddenSplitButton;
const newStateObject = this._effectiveStateObject;

if (!newStateObject) {
// eslint-disable-next-line no-console
console.warn(`State with name="${this.state}" doesn't exist!`);
} else if (button) {
const buttonWidth = button.offsetWidth;
const hiddenButton = this.shadowRoot?.querySelector(".ui5-ai-button-hidden") as MainButton;
button.style.width = `${buttonWidth}px`;
hiddenButton.icon = newStateObject.icon;
hiddenButton.endIcon = newStateObject.endIcon;
hiddenButton.textContent = newStateObject.text || null;

await renderFinished();
const hiddenButtonWidth = hiddenButton.offsetWidth;
this.fadeOut = true;
button.style.width = `${hiddenButtonWidth}px`;

setTimeout(() => {
this.fadeMid = true;
this._currentStateObject = newStateObject;
this._fadeIn();
}, fadeOutDuration);
return;
}

if (!button || !hiddenButton) {
return;
}

const buttonWidth = button.offsetWidth;
const currentState: Partial<ButtonState> = this._currentStateObject || {};

if ((!currentState.splitMode && newStateObject.splitMode) || (!currentState.endIcon && !!newStateObject.endIcon)) {
this.classList.add("ui5-ai-button-button-to-menu");
}
if ((currentState.splitMode && !newStateObject.splitMode) || (!!currentState.endIcon && !newStateObject.endIcon)) {
this.classList.add("ui5-ai-button-menu-to-button");
}

this.style.width = `${buttonWidth}px`;
hiddenButton.icon = newStateObject.icon;
hiddenButton._endIcon = newStateObject.endIcon;
hiddenButton.textContent = newStateObject.text || null;
hiddenButton._hideArrowButton = this._hideArrowButton;

await renderFinished();
const hiddenButtonWidth = hiddenButton.offsetWidth;
this.style.width = `${hiddenButtonWidth}px`;
this.classList.add("ui5-ai-button-fade-out");

setTimeout(() => {
this.classList.add("ui5-ai-button-fade-mid");
button._hideArrowButton = this._hideArrowButton;
this._fadeIn();
}, fadeOutDuration);
}

/**
* Starts the fade-in animation.
* @private
*/
_fadeIn(): void {
const fadeInDuration = 60;
const fadeInDuration = 160;

setTimeout(() => {
this.fadeIn = true;
const newStateObject = this._effectiveStateObject;
this._currentStateObject = newStateObject;
this.classList.add("ui5-ai-button-fade-in");
this._resetFade();
}, fadeInDuration);
}
Expand All @@ -203,13 +274,16 @@ class Button extends UI5Element {
const fadeResetDuration = 160;

setTimeout(() => {
this.fadeOut = false;
this.fadeMid = false;
this.fadeIn = false;
this.classList.remove("ui5-ai-button-fade-out");
this.classList.remove("ui5-ai-button-fade-mid");
this.classList.remove("ui5-ai-button-fade-in");
this.classList.remove("ui5-ai-button-button-to-menu");
this.classList.remove("ui5-ai-button-menu-to-button");
}, fadeResetDuration);

// reset the button's width after animations
const button = this._mainButton;
const button = this._splitButton;

if (button) {
button.style.width = "";
}
Expand All @@ -219,41 +293,18 @@ class Button extends UI5Element {
* Handles the click event.
* @private
*/
_onclick(e: MouseEvent): void {
_onClick(e: CustomEvent): void {
e.stopImmediatePropagation();
this.fireDecoratorEvent("click");
}

get _mainButton() {
return this.shadowRoot?.querySelector("[ui5-button]") as MainButton;
}

get _effectiveState() {
return this.state || (this.states.length && this.states[0].name) || "";
}

get _effectiveStateObject() {
return this.states.find(state => state.name === this._effectiveState);
}

get _stateIconOnly() {
return !this._stateText && !!this._stateIcon;
}

get _stateText() {
return this._currentStateObject?.text;
}

get _stateIcon() {
return this._currentStateObject?.icon;
}

get _stateEndIcon() {
return this._currentStateObject?.endIcon;
}

get _hasText() {
return !!this._stateText;
/**
* Handles the arrow-click event when `ui5-ai-button` is in split mode.
* @private
*/
_onArrowClick(e: CustomEvent): void {
e.stopImmediatePropagation();
this.fireDecoratorEvent("arrow-click");
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/ai/src/ButtonState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ class ButtonState extends UI5Element {
*/
@property()
endIcon?: string;

/**
* Defines if the component is in split button mode.
* @default false
* @since 2.6.0
* @public
*/
@property({ type: Boolean })
splitMode = false;
}

ButtonState.define();
Expand Down
14 changes: 8 additions & 6 deletions packages/ai/src/ButtonTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import MainButton from "@ui5/webcomponents/dist/Button.js";
import SplitButton from "@ui5/webcomponents/dist/SplitButton.js";
import type Button from "./Button.js";

export default function ButtonTemplate(this: Button) {
return (<>
<MainButton
<SplitButton
class="ui5-ai-button-inner"
design={this.design}
icon={this._stateIcon}
endIcon={this._stateEndIcon}
disabled={this.disabled}
onClick={this._onclick}
_endIcon={this._stateEndIcon}
_hideArrowButton={this._hideArrowButton}
onClick={this._onClick}
onArrowClick={this._onArrowClick}
>
{this._hasText && (
<div class="ui5-ai-button-text">{this._stateText}</div>
)}
</MainButton>
</SplitButton>

<MainButton
<SplitButton
class="ui5-ai-button-hidden"
design={this.design}
/>
Expand Down
Loading
Loading