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 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.
+
+ 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.
+
InputIcon components with different use cases, all with consistent button-like styling.
+ +InputIcon works correctly in both Cozy and Compact modes.
+ ++ InputIcon is focusable and keyboard-accessible. Try tabbing through the input below + and pressing Enter/Space on the icon. +
+ +InputIcon works consistently whether or not the clear icon is present.
+ ++ 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. +
+ ++ 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. +
+ +