Skip to content
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
184 changes: 184 additions & 0 deletions packages/main/src/InputIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
import InputIconTemplate from "./InputIconTemplate.js";
import inputIconCss from "./generated/themes/InputIcon.css.js";
import "./Icon.js";

/**
* @class
* ### Overview
* The `ui5-input-icon` component represents an interactive icon that can be placed inside an `ui5-input` component.
* Unlike the standard `ui5-icon`, this component provides button-like behavior with hover, focus, and active states,
* matching the visual style of the input's built-in clear icon.
*
* ### Usage
* Use `ui5-input-icon` for interactive icons that users can click (e.g., search, voice input, camera).
* For decorative icons, use the standard `ui5-icon` component instead.
*
* ### ES6 Module Import
* `import "@ui5/webcomponents/dist/InputIcon.js";`
*
* @constructor
* @extends UI5Element
* @public
* @since 2.24.0
*/
@customElement({
tag: "ui5-input-icon",
renderer: jsxRenderer,
template: InputIconTemplate,
styles: inputIconCss,
languageAware: false,
themeAware: true,
})
/**
* Fired when the `ui5-input-icon` is activated either with a click/tap or by using the Enter or Space key.
* @public
*/
@event("click", {
bubbles: true,
})
class InputIcon extends UI5Element {
eventDetails!: {
click: void;
}

/**
* Defines the icon name to be displayed.
*
* **Note:** Make sure you import the desired icon before using it.
*
* @default undefined
* @public
*/
@property()
name?: string;

/**
* Defines the accessible name of the icon.
*
* **Note:** This property is used for accessibility purposes and will be announced by screen readers.
*
* @default undefined
* @public
*/
@property()
accessibleName?: string;

/**
* Defines whether the tooltip is shown.
*
* **Note:** The tooltip text should be provided via the `accessible-name` property.
*
* @default false
* @public
*/
@property({ type: Boolean })
showTooltip = false;

/**
* Defines the value state of the icon.
*
* **Note:** This property should match the parent input's value state for consistent styling.
*
* @default "None"
* @public
*/
@property()
valueState: `${ValueState}` = "None";

/**
* Defines whether the icon is disabled.
*
* **Note:** Disabled icons are not interactive and do not fire click events.
*
* @default false
* @public
*/
@property({ type: Boolean })
disabled = false;

/**
* @private
*/
@property({ type: Boolean, noAttribute: true })
_pressed = false;

/**
* @private
*/
@property({ type: Boolean, noAttribute: true })
_focused = false;

_onclick() {
if (!this.disabled) {
this.fireDecoratorEvent("click");
}
}

_onmousedown() {
if (!this.disabled) {
this._pressed = true;
}
}

_onmouseup() {
this._pressed = false;
}

_onmouseleave() {
this._pressed = false;
}

_onfocus() {
this._focused = true;
}

_onblur() {
this._focused = false;
this._pressed = false;
}

_onkeydown(e: KeyboardEvent) {
if (this.disabled) {
return;
}

if (isEnter(e) || isSpace(e)) {
this._pressed = true;
e.preventDefault(); // Prevent scrolling on Space
}
}

_onkeyup(e: KeyboardEvent) {
if (this.disabled) {
return;
}

if (isEnter(e) || isSpace(e)) {
this._pressed = false;
this.fireDecoratorEvent("click");
}
}

get effectiveTabIndex() {
return this.disabled ? -1 : 0;
}

get effectiveAriaLabel() {
return this.accessibleName || undefined;
}

get effectiveTitle() {
return this.showTooltip && this.accessibleName ? this.accessibleName : undefined;
}
}

InputIcon.define();

export default InputIcon;
39 changes: 39 additions & 0 deletions packages/main/src/InputIconTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type InputIcon from "./InputIcon.js";
import Icon from "./Icon.js";

export default function InputIconTemplate(this: InputIcon) {
return (
<div
class={{
"ui5-input-icon-root": true,
"inputIcon": true,
"inputIcon--pressed": this._pressed,
"inputIcon--focused": this._focused,
"inputIcon--disabled": this.disabled,
}}
role="button"
tabindex={this.effectiveTabIndex}
aria-label={this.effectiveAriaLabel}
aria-pressed={this._pressed}
aria-disabled={this.disabled}
title={this.effectiveTitle}
onClick={this._onclick}
onMouseDown={this._onmousedown}
onMouseUp={this._onmouseup}
onMouseLeave={this._onmouseleave}
onFocus={this._onfocus}
onBlur={this._onblur}
onKeyDown={this._onkeydown}
onKeyUp={this._onkeyup}
part="root"
>
{this.name && (
<Icon
name={this.name}
class="ui5-input-icon-inner"
aria-hidden="true"
/>
)}
</div>
);
}
1 change: 1 addition & 0 deletions packages/main/src/bundle.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import TableRowAction from "./TableRowAction.js";
import TableRowActionNavigation from "./TableRowActionNavigation.js";
import Icon from "./Icon.js";
import Input from "./Input.js";
import InputIcon from "./InputIcon.js";
import SuggestionItemCustom from "./SuggestionItemCustom.js";
import MultiInput from "./MultiInput.js";
import Label from "./Label.js";
Expand Down
84 changes: 84 additions & 0 deletions packages/main/src/themes/InputIcon.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
:host {
align-self: stretch;
height: 100%;
}

.ui5-input-icon-root {
height: 100%;
box-sizing: border-box;
}

.inputIcon {
color: var(--_ui5_input_icon_color);
cursor: pointer;
Expand All @@ -7,6 +17,14 @@
min-width: 1rem;
min-height: 1rem;
border-radius: var(--_ui5_input_icon_border_radius);
display: flex;
align-items: center;
justify-content: center;
}

.inputIcon:focus-visible {
outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor);
outline-offset: -0.125rem;
}

.inputIcon.inputIcon--pressed {
Expand All @@ -32,3 +50,69 @@
border-inline-start: var(--_ui5_select_hover_icon_left_border);
box-shadow: var(--_ui5_input_icon_box_shadow);
}

.inputIcon--disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}

/* Style the inner Icon component */
.ui5-input-icon-inner {
display: flex;
width: 1rem;
height: 1rem;
color: inherit;
pointer-events: none;
flex-shrink: 0;
}

/* Value state styling */
:host([value-state="Negative"]) .inputIcon,
:host([value-state="Critical"]) .inputIcon {
padding: var(--_ui5_input_error_warning_icon_padding);
}

:host([value-state="Information"]) .inputIcon {
padding: var(--_ui5_input_information_icon_padding);
}

:host([value-state="Negative"]) .inputIcon:active,
:host([value-state="Negative"]) .inputIcon.inputIcon--pressed {
box-shadow: var(--_ui5_input_error_icon_box_shadow);
color: var(--_ui5_input_icon_error_pressed_color);
}

:host([value-state="Negative"]) .inputIcon:not(.inputIcon--pressed):not(:active):hover {
box-shadow: var(--_ui5_input_error_icon_box_shadow);
}

:host([value-state="Critical"]) .inputIcon:active,
:host([value-state="Critical"]) .inputIcon.inputIcon--pressed {
box-shadow: var(--_ui5_input_warning_icon_box_shadow);
color: var(--_ui5_input_icon_warning_pressed_color);
}

:host([value-state="Critical"]) .inputIcon:not(.inputIcon--pressed):not(:active):hover {
box-shadow: var(--_ui5_input_warning_icon_box_shadow);
}

:host([value-state="Information"]) .inputIcon:active,
:host([value-state="Information"]) .inputIcon.inputIcon--pressed {
box-shadow: var(--_ui5_input_information_icon_box_shadow);
color: var(--_ui5_input_icon_information_pressed_color);
}

:host([value-state="Information"]) .inputIcon:not(.inputIcon--pressed):not(:active):hover {
box-shadow: var(--_ui5_input_information_icon_box_shadow);
}

:host([value-state="Positive"]) .inputIcon:active,
:host([value-state="Positive"]) .inputIcon.inputIcon--pressed {
box-shadow: var(--_ui5_input_success_icon_box_shadow);
color: var(--_ui5_input_icon_success_pressed_color);
}

:host([value-state="Positive"]) .inputIcon:not(.inputIcon--pressed):not(:active):hover {
box-shadow: var(--_ui5_input_success_icon_box_shadow);
}
Loading
Loading