diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts new file mode 100644 index 000000000000..e710113de14f --- /dev/null +++ b/packages/main/src/InputIcon.ts @@ -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; diff --git a/packages/main/src/InputIconTemplate.tsx b/packages/main/src/InputIconTemplate.tsx new file mode 100644 index 000000000000..ab1aaff1ca09 --- /dev/null +++ b/packages/main/src/InputIconTemplate.tsx @@ -0,0 +1,39 @@ +import type InputIcon from "./InputIcon.js"; +import Icon from "./Icon.js"; + +export default function InputIconTemplate(this: InputIcon) { + return ( +
+ {this.name && ( +
+ ); +} diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index f770e818a792..c550fa336994 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -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"; diff --git a/packages/main/src/themes/InputIcon.css b/packages/main/src/themes/InputIcon.css index 5d17e767ed34..8242ac0e54a8 100644 --- a/packages/main/src/themes/InputIcon.css +++ b/packages/main/src/themes/InputIcon.css @@ -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; @@ -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 { @@ -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); +} diff --git a/packages/main/test/pages/InputInteractiveIcons_POC.html b/packages/main/test/pages/InputInteractiveIcons_POC.html new file mode 100644 index 000000000000..b2b574beedc2 --- /dev/null +++ b/packages/main/test/pages/InputInteractiveIcons_POC.html @@ -0,0 +1,519 @@ + + + + + + + POC: Input Interactive Icons Harmonization + + + + + + + +

POC: InputIcon Component - Interactive Icons for Input Fields

+

+ This page demonstrates the new <ui5-input-icon> component - a public, standalone component + for displaying interactive icons in input fields. It provides button-like behavior with hover, focus, and + active states, harmonizing with the built-in clear icon. Use <ui5-input-icon> for + interactive icons and <ui5-icon> for decorative icons. +

+ +
+

Theme Switcher

+ Horizon + Horizon Dark + Horizon HCB + Horizon HCW + Fiori 3 + Fiori 3 Dark + Fiori 3 HCB + Fiori 3 HCW +
+

Content Density

+ Toggle Compact +
+ + +
+

1. Visual Comparison

+

+ The inputs below demonstrate the difference between interactive icons (<ui5-input-icon>) + and decorative icons (<ui5-icon>). Notice how InputIcon has button-like hover/focus/active + states matching the clear icon. +

+ +
+
+

With InputIcon (Interactive)

+
+
+ Normal state: + + + +
+
+ Negative state: + + + +
+
+ Critical state: + + + +
+
+ Positive state: + + + +
+
+
+ +
+

With Icon (Decorative)

+
+
+ Normal state: + + + +
+
+ Negative state: + + + +
+
+ Critical state: + + + +
+
+ Positive state: + + + +
+
+
+
+
+ + +
+

2. Various InputIcon Examples

+

InputIcon components with different use cases, all with consistent button-like styling.

+ +
+
+ Search icon: + + + +
+
+ Voice input icon: + + + +
+
+ Camera icon: + + + +
+
+ Navigation icon: + + + +
+
+ Edit icon: + + + +
+
+
+ + +
+

3. Density Mode Support

+

InputIcon works correctly in both Cozy and Compact modes.

+ +

Cozy Mode (Default)

+
+
+ + + +
+
+ +

Compact Mode

+
+
+ + + +
+
+
+ + +
+

4. Accessibility & Keyboard Navigation

+

+ InputIcon is focusable and keyboard-accessible. Try tabbing through the input below + and pressing Enter/Space on the icon. +

+ +
+
+ + + +
+
+ +
+

Event Log:

+
+
+
+ + +
+

5. InputIcon Without Clear Icon

+

InputIcon works consistently whether or not the clear icon is present.

+ +
+
+ Single icon: + + + +
+
+ With clear icon: + + + +
+
+
+ + +
+

6. Multiple Icons Support

+

+ Testing multiple icons in one input. Use InputIcon for interactive icons that users can click, + and regular Icon for decorative icons. Each icon type maintains its appropriate styling. +

+ +
+
+ Multiple decorative: + + + + +
+
+ Multiple interactive: + + + + +
+
+ Mixed modes: + + + + +
+
+ Three interactive: + + + + + +
+
+
+ + +
+

7. Custom Height Inputs

+

+ InputIcon scales correctly with custom input heights - both small (24px) and large (64px). + The icons maintain proper visual alignment and interactive behavior at all sizes. +

+ +

Small Height (24px)

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +

Big Height (64px)

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + + +