From 019957451301aba73dd486b4be33a5763a936bee Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Fri, 22 May 2026 14:11:13 +0300 Subject: [PATCH 1/4] feat(ui5-input): add styling to interactive custom icons --- packages/main/src/Input.ts | 23 + packages/main/src/themes/Icon.css | 11 + packages/main/src/themes/Input.css | 129 ++++-- .../test/pages/InputInteractiveIcons_POC.html | 426 ++++++++++++++++++ 4 files changed, 559 insertions(+), 30 deletions(-) create mode 100644 packages/main/test/pages/InputInteractiveIcons_POC.html diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index e6a83907a0d8..540d23c50c69 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -816,6 +816,9 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } onAfterRendering() { + // Add class to interactive icons for styling + this._styleInteractiveIcons(); + if (this.showSuggestions && this.Suggestions?._getPicker()) { this._listWidth = this.Suggestions._getListWidth(); @@ -846,6 +849,26 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } } + _styleInteractiveIcons() { + // Add a class to interactive icons so CSS can style them + this.icon.forEach(iconEl => { + const isInteractive = (iconEl as any).mode === "Interactive"; + if (isInteractive) { + iconEl.classList.add("ui5-input-icon-interactive"); + // Make the icon host focusable so clicking anywhere (including padding) focuses it + iconEl.setAttribute("tabindex", "0"); + // Remove tabindex from the SVG inside so only the host is focusable + const svg = iconEl.shadowRoot?.querySelector("svg"); + if (svg) { + svg.removeAttribute("tabindex"); + } + } else { + iconEl.classList.remove("ui5-input-icon-interactive"); + iconEl.removeAttribute("tabindex"); + } + }); + } + _adjustSelectionRange() { const innerInput = this.getInputDOMRefSync()!; const visibleItems = this.Suggestions?._getItems().filter(item => !item.hidden) as IInputSuggestionItemSelectable[]; diff --git a/packages/main/src/themes/Icon.css b/packages/main/src/themes/Icon.css index 6f928c35fb8c..19e58e42783c 100644 --- a/packages/main/src/themes/Icon.css +++ b/packages/main/src/themes/Icon.css @@ -56,6 +56,17 @@ border-radius: var(--ui5-icon-focus-border-radius); } +/* Interactive icons inside Input - constrain SVG size to 1rem and suppress focus (Input handles it) */ +:host(.ui5-input-icon-interactive) .ui5-icon-root { + height: 1rem; + width: 1rem; +} + +:host(.ui5-input-icon-interactive[desktop]) .ui5-icon-root:focus, +:host(.ui5-input-icon-interactive) .ui5-icon-root:focus-visible { + outline: none; +} + .ui5-icon-root { display:flex; height: 100%; diff --git a/packages/main/src/themes/Input.css b/packages/main/src/themes/Input.css index eeadb34e3642..c3625af19cab 100644 --- a/packages/main/src/themes/Input.css +++ b/packages/main/src/themes/Input.css @@ -389,53 +389,122 @@ align-items: center; } -/* TODO: Remove this after parser is fixed - - this statement is transformed to [ui5-multi-combobox] [ui5-icon] which - affects all icons in the combobox incuding these in the list items -*/ -::slotted([ui5-icon][slot="icon"]) { +/* Style interactive icons directly - they look like buttons */ +/* Interactive icons with button-like styling */ +::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { align-self: start; - padding: var(--_ui5_input_custom_icon_padding); - /* Normalize like libraries overrule the selector, thefore we need !important */ - box-sizing: content-box !important; + color: var(--_ui5_input_icon_color); + cursor: pointer; + padding: 0; + width: var(--_ui5_input_icon_width); + min-width: var(--_ui5_input_icon_width); + height: var(--_ui5_input_icon_wrapper_height); + display: flex; + align-items: center; + justify-content: center; + background-color: transparent; + border-radius: var(--_ui5_input_icon_border_radius); + transition: background 0.1s ease-in-out; + outline: none; + box-sizing: border-box; + font-size: 1rem; + flex-shrink: 0; +} + +::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { + background: var(--_ui5_input_icon_hover_bg); + box-shadow: var(--_ui5_input_icon_box_shadow); +} + +::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), +::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { + outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); + outline-offset: -2px; + background: var(--_ui5_input_icon_hover_bg); + box-shadow: var(--_ui5_input_icon_box_shadow); +} + +::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:active) { + background-color: var(--sapButton_Active_Background); + color: var(--_ui5_input_icon_pressed_color); + box-shadow: var(--_ui5_input_icon_box_shadow); + border-inline-start: var(--_ui5_select_hover_icon_left_border); +} + +/* Value state specific focus colors */ +:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), +:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { + outline-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); + box-shadow: var(--_ui5_input_error_icon_box_shadow); +} + +:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), +:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { + outline-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); + box-shadow: var(--_ui5_input_warning_icon_box_shadow); +} + +:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), +:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { + outline-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); + box-shadow: var(--_ui5_input_success_icon_box_shadow); +} + +:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), +:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { + box-shadow: var(--_ui5_input_information_icon_box_shadow); +} + +/* Value state specific hover box-shadows */ +:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { + box-shadow: var(--_ui5_input_error_icon_box_shadow); +} + +:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { + box-shadow: var(--_ui5_input_warning_icon_box_shadow); +} + +:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { + box-shadow: var(--_ui5_input_success_icon_box_shadow); } -:host([value-state="Negative"]) .inputIcon, -:host([value-state="Critical"]) .inputIcon{ - padding: var(--_ui5_input_error_warning_icon_padding); +:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { + box-shadow: var(--_ui5_input_information_icon_box_shadow); } -:host([value-state="Negative"][focused]) .inputIcon, -:host([value-state="Critical"][focused]) .inputIcon{ - padding: var(--_ui5_input_error_warning_focused_icon_padding); +/* Decorative icons keep their normal styling */ +::slotted([ui5-icon][slot="icon"]:not(.ui5-input-icon-interactive)) { + align-self: start; + padding: var(--_ui5_input_custom_icon_padding); + box-sizing: content-box; + color: var(--_ui5_input_icon_color); } -:host([value-state="Information"]) .inputIcon { - padding: var(--_ui5_input_information_icon_padding); +/* Adjust height for value states with thicker borders (Negative, Critical, Information) */ +:host([value-state]:not([value-state="None"]):not([value-state="Positive"])) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_state_height); } -:host([value-state="Information"][focused]) .inputIcon { - padding: var(--_ui5_input_information_focused_icon_padding); +/* Adjust height for Positive state */ +:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_success_state_height); } -:host([value-state="Negative"]) ::slotted(.inputIcon[ui5-icon]), -:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"]), -:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"]) { - padding: var(--_ui5_input_error_warning_custom_icon_padding); +/* Value state styling for interactive icons - pressed color */ +:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_error_pressed_color); } -:host([value-state="Negative"][focused]) ::slotted(.inputIcon[ui5-icon]), -:host([value-state="Negative"][focused]) ::slotted([ui5-icon][slot="icon"]), -:host([value-state="Critical"][focused]) ::slotted([ui5-icon][slot="icon"]) { - padding: var(--_ui5_input_error_warning_custom_focused_icon_padding); +:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_warning_pressed_color); } -:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"]) { - padding: var(--_ui5_input_information_custom_icon_padding); +:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_information_pressed_color); } -:host([value-state="Information"][focused]) ::slotted([ui5-icon][slot="icon"]) { - padding: var(--_ui5_input_information_custom_focused_icon_padding); +:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_success_pressed_color); } :host([value-state="Negative"]) .inputIcon:active, diff --git a/packages/main/test/pages/InputInteractiveIcons_POC.html b/packages/main/test/pages/InputInteractiveIcons_POC.html new file mode 100644 index 000000000000..14361d688760 --- /dev/null +++ b/packages/main/test/pages/InputInteractiveIcons_POC.html @@ -0,0 +1,426 @@ + + + + + + + POC: Input Interactive Icons Harmonization + + + + + + +

POC: Harmonize Input Interactive Icons Styling

+

+ This page demonstrates the POC for issue #6132 - harmonizing the visual styling of user-provided + interactive icons with the built-in clear icon. The solution automatically applies the + .inputIcon styling to custom icons when they have mode="Interactive". +

+ +
+

Theme Switcher

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

1. Visual Comparison

+

+ The inputs below demonstrate the difference between interactive and decorative icons. + Notice how interactive icons now have hover states matching the clear icon. +

+ +
+
+

With Interactive Icon (mode="Interactive")

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

With Decorative Icon (mode="Decorative")

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

2. Various Interactive Icon Examples

+

Interactive icons 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

+

Interactive icons work correctly in both Cozy and Compact modes.

+ +

Cozy Mode (Default)

+
+
+ + + +
+
+ +

Compact Mode

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

4. Accessibility & Keyboard Navigation

+

+ Interactive icons are focusable and keyboard-accessible. Try tabbing through the input below + and pressing Enter/Space on the icon. +

+ +
+
+ + + +
+
+ +
+

Event Log:

+
+
+
+ + +
+

5. Interactive Icons Without Clear Icon

+

Interactive icons work consistently whether or not the clear icon is present.

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

6. Multiple Icons Support

+

+ Testing multiple icons in one input. Each icon gets its own wrapper, allowing individual + hover and focus effects. Interactive icons are styled like the clear icon with button-like appearance. +

+ +
+
+ Multiple decorative: + + + + +
+
+ Multiple interactive: + + + + +
+
+ Mixed modes: + + + + +
+
+ Three interactive: + + + + + +
+
+
+ + + + + From 7cd4107f2450cb9795820b5220d42c21c91eb47c Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Mon, 1 Jun 2026 10:05:52 +0300 Subject: [PATCH 2/4] feat(ui5-input): add styling to interactive custom icons --- packages/main/src/Input.ts | 25 +---- packages/main/src/InputIcon.ts | 101 +++++++++++++++++ packages/main/src/InputIconTemplate.tsx | 22 ++++ packages/main/src/InputTemplate.tsx | 42 ++++---- packages/main/src/themes/Icon.css | 11 -- packages/main/src/themes/Input.css | 122 +-------------------- packages/main/src/themes/InputIcon.css | 138 ++++++++++++++++++++---- 7 files changed, 268 insertions(+), 193 deletions(-) create mode 100644 packages/main/src/InputIcon.ts create mode 100644 packages/main/src/InputIconTemplate.tsx diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 540d23c50c69..af2541e5c5f0 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -604,7 +604,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement * Defines the icon to be displayed in the component. * @public */ - @slot() + @slot({ type: HTMLElement, individualSlots: true }) icon!: Slot; /** @@ -816,9 +816,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } onAfterRendering() { - // Add class to interactive icons for styling - this._styleInteractiveIcons(); - if (this.showSuggestions && this.Suggestions?._getPicker()) { this._listWidth = this.Suggestions._getListWidth(); @@ -849,26 +846,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } } - _styleInteractiveIcons() { - // Add a class to interactive icons so CSS can style them - this.icon.forEach(iconEl => { - const isInteractive = (iconEl as any).mode === "Interactive"; - if (isInteractive) { - iconEl.classList.add("ui5-input-icon-interactive"); - // Make the icon host focusable so clicking anywhere (including padding) focuses it - iconEl.setAttribute("tabindex", "0"); - // Remove tabindex from the SVG inside so only the host is focusable - const svg = iconEl.shadowRoot?.querySelector("svg"); - if (svg) { - svg.removeAttribute("tabindex"); - } - } else { - iconEl.classList.remove("ui5-input-icon-interactive"); - iconEl.removeAttribute("tabindex"); - } - }); - } - _adjustSelectionRange() { const innerInput = this.getInputDOMRefSync()!; const visibleItems = this.Suggestions?._getItems().filter(item => !item.hidden) as IInputSuggestionItemSelectable[]; diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts new file mode 100644 index 000000000000..75c10ffca6ac --- /dev/null +++ b/packages/main/src/InputIcon.ts @@ -0,0 +1,101 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import jsxRender from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import type { IIcon } from "./Icon.js"; +import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import InputIconTemplate from "./InputIconTemplate.js"; + +// Styles +import inputIconCss from "./generated/themes/InputIcon.css.js"; + +/** + * @class + * + * ### Overview + * The `ui5-input-icon` is an internal component used by `ui5-input` to wrap interactive icons + * with button-like styling and behavior. + * + * @constructor + * @extends UI5Element + * @private + */ +@customElement({ + tag: "ui5-input-icon", + renderer: jsxRender, + template: InputIconTemplate, + styles: inputIconCss, +}) +class InputIcon extends UI5Element { + /** + * Defines the name of the icon to display. + * If provided, InputIcon will create the icon internally. + * If not provided, expects an icon to be slotted. + * @default undefined + * @public + */ + @property() + iconName?: string; + + /** + * Defines the accessible name of the icon. + * @default undefined + * @public + */ + @property() + accessibleName?: string; + + /** + * Defines the value state of the Input that the icon belongs to. + * Used for styling the icon according to the input's state. + * @default "None" + * @private + */ + @property() + valueState?: string = "None"; + + /** + * Defines the icon element (when not using iconName). + * @default [] + * @public + */ + @slot({ type: HTMLElement, "default": true }) + icon!: DefaultSlot; + + onAfterRendering() { + // Make icons non-focusable - the wrapper handles all focus and interaction + // Handle both slotted icons and internally created icon (via iconName) + + // Handle slotted icons + if (this.icon && this.icon.length > 0) { + this.icon.forEach(iconEl => { + // Set tabindex="-1" on the icon host to make it non-focusable + iconEl.setAttribute("tabindex", "-1"); + // Set tabindex="-1" on the SVG inside + const svg = iconEl.shadowRoot?.querySelector("svg"); + if (svg) { + svg.setAttribute("tabindex", "-1"); + } + }); + } + + // Handle internally created icon (when iconName is used) + if (this.iconName) { + const internalIcon = this.shadowRoot?.querySelector("[ui5-icon]") as HTMLElement; + if (internalIcon) { + // Set tabindex="-1" on the icon host to make it non-focusable + internalIcon.setAttribute("tabindex", "-1"); + // Set tabindex="-1" on the SVG inside + const svg = internalIcon.shadowRoot?.querySelector("svg"); + if (svg) { + svg.setAttribute("tabindex", "-1"); + } + } + } + } +} + +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..7d4cc23f57f2 --- /dev/null +++ b/packages/main/src/InputIconTemplate.tsx @@ -0,0 +1,22 @@ +import type InputIcon from "./InputIcon.js"; +import Icon from "./Icon.js"; + +export default function InputIconTemplate(this: InputIcon) { + return ( +
+ {this.iconName ? ( + + ) : ( + + )} +
+ ); +} diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index 4a6ce63f14fc..27d12bb8bb1d 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -1,11 +1,17 @@ import type Input from "./Input.js"; import type { JsxTemplateResult } from "@ui5/webcomponents-base/dist/index.js"; -import Icon from "./Icon.js"; +import type { IIcon } from "./Icon.js"; +import InputIcon from "./InputIcon.js"; import decline from "@ui5/webcomponents-icons/dist/decline.js"; import InputPopoverTemplate from "./InputPopoverTemplate.js"; type TemplateHook = () => JsxTemplateResult; +type SlottedIcon = IIcon & { + _individualSlot: string; + accessibleName?: string; +}; + export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook, mobileHeader?: TemplateHook }) { const suggestionsList = hooks?.suggestionsList; const mobileHeader = hooks?.mobileHeader; @@ -63,29 +69,27 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat /> {this._effectiveShowClearIcon && -
- - -
+ /> } {this.icon.length > 0 && -
- -
+ this.icon.map(iconEl => { + const slottedIcon = iconEl as SlottedIcon; + return ( + + + + ); + }) }
diff --git a/packages/main/src/themes/Icon.css b/packages/main/src/themes/Icon.css index d310ea7cc9dc..18711ecb8510 100644 --- a/packages/main/src/themes/Icon.css +++ b/packages/main/src/themes/Icon.css @@ -57,17 +57,6 @@ border-radius: var(--ui5-icon-focus-border-radius); } -/* Interactive icons inside Input - constrain SVG size to 1rem and suppress focus (Input handles it) */ -:host(.ui5-input-icon-interactive) .ui5-icon-root { - height: 1rem; - width: 1rem; -} - -:host(.ui5-input-icon-interactive[desktop]) .ui5-icon-root:focus, -:host(.ui5-input-icon-interactive) .ui5-icon-root:focus-visible { - outline: none; -} - .ui5-icon-root { display:flex; height: 100%; diff --git a/packages/main/src/themes/Input.css b/packages/main/src/themes/Input.css index c3625af19cab..ffc72ebe49b7 100644 --- a/packages/main/src/themes/Input.css +++ b/packages/main/src/themes/Input.css @@ -379,134 +379,14 @@ box-shadow: var(--sapField_Hover_InformationShadow); } -/* Input Icon */ - -.ui5-input-icon-root { - min-width: var(--_ui5_input_icon_min_width); - height: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -/* Style interactive icons directly - they look like buttons */ -/* Interactive icons with button-like styling */ -::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - align-self: start; - color: var(--_ui5_input_icon_color); - cursor: pointer; - padding: 0; - width: var(--_ui5_input_icon_width); - min-width: var(--_ui5_input_icon_width); - height: var(--_ui5_input_icon_wrapper_height); - display: flex; - align-items: center; - justify-content: center; - background-color: transparent; - border-radius: var(--_ui5_input_icon_border_radius); - transition: background 0.1s ease-in-out; - outline: none; - box-sizing: border-box; - font-size: 1rem; - flex-shrink: 0; -} - -::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { - background: var(--_ui5_input_icon_hover_bg); - box-shadow: var(--_ui5_input_icon_box_shadow); -} - -::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), -::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { - outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); - outline-offset: -2px; - background: var(--_ui5_input_icon_hover_bg); - box-shadow: var(--_ui5_input_icon_box_shadow); -} - -::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:active) { - background-color: var(--sapButton_Active_Background); - color: var(--_ui5_input_icon_pressed_color); - box-shadow: var(--_ui5_input_icon_box_shadow); - border-inline-start: var(--_ui5_select_hover_icon_left_border); -} - -/* Value state specific focus colors */ -:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), -:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { - outline-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); - box-shadow: var(--_ui5_input_error_icon_box_shadow); -} - -:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), -:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { - outline-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); - box-shadow: var(--_ui5_input_warning_icon_box_shadow); -} - -:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), -:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { - outline-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); - box-shadow: var(--_ui5_input_success_icon_box_shadow); -} - -:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus), -:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:focus-visible) { - box-shadow: var(--_ui5_input_information_icon_box_shadow); -} - -/* Value state specific hover box-shadows */ -:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { - box-shadow: var(--_ui5_input_error_icon_box_shadow); -} - -:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { - box-shadow: var(--_ui5_input_warning_icon_box_shadow); -} - -:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { - box-shadow: var(--_ui5_input_success_icon_box_shadow); -} - -:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive:hover) { - box-shadow: var(--_ui5_input_information_icon_box_shadow); -} - /* Decorative icons keep their normal styling */ -::slotted([ui5-icon][slot="icon"]:not(.ui5-input-icon-interactive)) { +::slotted([ui5-icon][slot="icon"]) { align-self: start; padding: var(--_ui5_input_custom_icon_padding); box-sizing: content-box; color: var(--_ui5_input_icon_color); } -/* Adjust height for value states with thicker borders (Negative, Critical, Information) */ -:host([value-state]:not([value-state="None"]):not([value-state="Positive"])) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_state_height); -} - -/* Adjust height for Positive state */ -:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_success_state_height); -} - -/* Value state styling for interactive icons - pressed color */ -:host([value-state="Negative"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_error_pressed_color); -} - -:host([value-state="Critical"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_warning_pressed_color); -} - -:host([value-state="Information"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_information_pressed_color); -} - -:host([value-state="Positive"]) ::slotted([ui5-icon][slot="icon"].ui5-input-icon-interactive) { - --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_success_pressed_color); -} - :host([value-state="Negative"]) .inputIcon:active, :host([value-state="Negative"]) .inputIcon.inputIcon--pressed { box-shadow: var(--_ui5_input_error_icon_box_shadow); diff --git a/packages/main/src/themes/InputIcon.css b/packages/main/src/themes/InputIcon.css index 5d17e767ed34..f9b34a5c97c0 100644 --- a/packages/main/src/themes/InputIcon.css +++ b/packages/main/src/themes/InputIcon.css @@ -1,34 +1,136 @@ -.inputIcon { +:host { + display: inline-block; +} + +.ui5-input-icon-wrapper { color: var(--_ui5_input_icon_color); cursor: pointer; - outline: none; - padding: var(--_ui5_input_icon_padding); - border-inline-start: var(--_ui5_input_icon_border); - min-width: 1rem; - min-height: 1rem; + padding: 0; + width: var(--_ui5_input_icon_width); + min-width: var(--_ui5_input_icon_width); + height: var(--_ui5_input_icon_wrapper_height); + display: flex; + align-items: center; + justify-content: center; + background-color: transparent; border-radius: var(--_ui5_input_icon_border_radius); + transition: background 0.1s ease-in-out; + outline: none; + box-sizing: border-box; + font-size: 1rem; + flex-shrink: 0; } -.inputIcon.inputIcon--pressed { - background: var(--_ui5_input_icon_pressed_bg); +.ui5-input-icon-wrapper:hover { + background: var(--_ui5_input_icon_hover_bg); box-shadow: var(--_ui5_input_icon_box_shadow); - border-inline-start: var(--_ui5_select_hover_icon_left_border); - color: var(--_ui5_input_icon_pressed_color); } -.inputIcon:active { +.ui5-input-icon-wrapper:focus, +.ui5-input-icon-wrapper:focus-visible { + outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); + outline-offset: -2px; + background: var(--_ui5_input_icon_hover_bg); + box-shadow: var(--_ui5_input_icon_box_shadow); +} + +.ui5-input-icon-wrapper:active { background-color: var(--sapButton_Active_Background); + color: var(--_ui5_input_icon_pressed_color); box-shadow: var(--_ui5_input_icon_box_shadow); border-inline-start: var(--_ui5_select_hover_icon_left_border); - color: var(--_ui5_input_icon_pressed_color); } -.inputIcon:not(.inputIcon--pressed):not(:active):hover { - background: var(--_ui5_input_icon_hover_bg); - box-shadow: var(--_ui5_input_icon_box_shadow); +/* Value state specific focus colors */ +:host([value-state="Negative"]) .ui5-input-icon-wrapper:focus, +:host([value-state="Negative"]) .ui5-input-icon-wrapper:focus-visible { + outline-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); + box-shadow: var(--_ui5_input_error_icon_box_shadow); } -.inputIcon:hover { - border-inline-start: var(--_ui5_select_hover_icon_left_border); - box-shadow: var(--_ui5_input_icon_box_shadow); +:host([value-state="Critical"]) .ui5-input-icon-wrapper:focus, +:host([value-state="Critical"]) .ui5-input-icon-wrapper:focus-visible { + outline-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); + box-shadow: var(--_ui5_input_warning_icon_box_shadow); +} + +:host([value-state="Positive"]) .ui5-input-icon-wrapper:focus, +:host([value-state="Positive"]) .ui5-input-icon-wrapper:focus-visible { + outline-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); + box-shadow: var(--_ui5_input_success_icon_box_shadow); +} + +:host([value-state="Information"]) .ui5-input-icon-wrapper:focus, +:host([value-state="Information"]) .ui5-input-icon-wrapper:focus-visible { + box-shadow: var(--_ui5_input_information_icon_box_shadow); +} + +/* Value state specific hover box-shadows */ +:host([value-state="Negative"]) .ui5-input-icon-wrapper:hover { + box-shadow: var(--_ui5_input_error_icon_box_shadow); +} + +:host([value-state="Critical"]) .ui5-input-icon-wrapper:hover { + box-shadow: var(--_ui5_input_warning_icon_box_shadow); +} + +:host([value-state="Positive"]) .ui5-input-icon-wrapper:hover { + box-shadow: var(--_ui5_input_success_icon_box_shadow); +} + +:host([value-state="Information"]) .ui5-input-icon-wrapper:hover { + box-shadow: var(--_ui5_input_information_icon_box_shadow); +} + +/* Adjust height for value states with thicker borders */ +:host([value-state="Negative"]) .ui5-input-icon-wrapper, +:host([value-state="Critical"]) .ui5-input-icon-wrapper, +:host([value-state="Information"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_state_height); +} + +:host([value-state="Positive"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_wrapper_height: var(--_ui5_input_icon_wrapper_success_state_height); +} + +/* Value state specific pressed colors */ +:host([value-state="Negative"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_error_pressed_color); +} + +:host([value-state="Critical"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_warning_pressed_color); +} + +:host([value-state="Information"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_information_pressed_color); +} + +:host([value-state="Positive"]) .ui5-input-icon-wrapper { + --_ui5_input_icon_pressed_color: var(--_ui5_input_icon_success_pressed_color); +} + +/* Constrain slotted icon size to 1rem and suppress its focus (wrapper handles it) */ +::slotted([ui5-icon]) { + width: 1rem; + height: 1rem; + pointer-events: none; + color: inherit; /* Inherit color from wrapper */ +} + +/* Suppress icon SVG focus outline - wrapper handles focus */ +::slotted([ui5-icon])::part(root) { + outline: none; +} + +/* Internally created icon (via iconName) - suppress focus and pointer events */ +.ui5-input-icon-wrapper > [ui5-icon] { + width: 1rem; + height: 1rem; + pointer-events: none; + color: inherit; /* Inherit color from wrapper */ +} + +.ui5-input-icon-wrapper > [ui5-icon]::part(root) { + outline: none; } From 4d0decb2bd286f32e4ae1b91b0ad7d6926593260 Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Mon, 1 Jun 2026 14:21:41 +0300 Subject: [PATCH 3/4] feat(ui5-input): add styling to interactive custom icons --- packages/main/src/InputIcon.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts index 75c10ffca6ac..2645bcd17a09 100644 --- a/packages/main/src/InputIcon.ts +++ b/packages/main/src/InputIcon.ts @@ -33,7 +33,7 @@ class InputIcon extends UI5Element { * If provided, InputIcon will create the icon internally. * If not provided, expects an icon to be slotted. * @default undefined - * @public + * @private */ @property() iconName?: string; @@ -41,7 +41,7 @@ class InputIcon extends UI5Element { /** * Defines the accessible name of the icon. * @default undefined - * @public + * @private */ @property() accessibleName?: string; @@ -58,7 +58,7 @@ class InputIcon extends UI5Element { /** * Defines the icon element (when not using iconName). * @default [] - * @public + * @private */ @slot({ type: HTMLElement, "default": true }) icon!: DefaultSlot; From c35168d66e43087ec49019064262de8299233b1e Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Mon, 1 Jun 2026 14:31:19 +0300 Subject: [PATCH 4/4] feat(ui5-input): add styling to interactive custom icons --- packages/main/src/InputIcon.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts index 2645bcd17a09..8fd6871c1244 100644 --- a/packages/main/src/InputIcon.ts +++ b/packages/main/src/InputIcon.ts @@ -70,13 +70,7 @@ class InputIcon extends UI5Element { // Handle slotted icons if (this.icon && this.icon.length > 0) { this.icon.forEach(iconEl => { - // Set tabindex="-1" on the icon host to make it non-focusable - iconEl.setAttribute("tabindex", "-1"); - // Set tabindex="-1" on the SVG inside - const svg = iconEl.shadowRoot?.querySelector("svg"); - if (svg) { - svg.setAttribute("tabindex", "-1"); - } + this._makeIconNonFocusable(iconEl); }); } @@ -84,16 +78,25 @@ class InputIcon extends UI5Element { if (this.iconName) { const internalIcon = this.shadowRoot?.querySelector("[ui5-icon]") as HTMLElement; if (internalIcon) { - // Set tabindex="-1" on the icon host to make it non-focusable - internalIcon.setAttribute("tabindex", "-1"); - // Set tabindex="-1" on the SVG inside - const svg = internalIcon.shadowRoot?.querySelector("svg"); - if (svg) { - svg.setAttribute("tabindex", "-1"); - } + this._makeIconNonFocusable(internalIcon); } } } + + /** + * Makes an icon non-focusable by setting tabindex="-1" on both the icon host and its internal SVG. + * @param iconEl The icon element to make non-focusable + * @private + */ + _makeIconNonFocusable(iconEl: HTMLElement) { + // Set tabindex="-1" on the icon host to make it non-focusable + iconEl.setAttribute("tabindex", "-1"); + // Set tabindex="-1" on the SVG inside + const svg = iconEl.shadowRoot?.querySelector("svg"); + if (svg) { + svg.setAttribute("tabindex", "-1"); + } + } } InputIcon.define();