From 019957451301aba73dd486b4be33a5763a936bee Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Fri, 22 May 2026 14:11:13 +0300 Subject: [PATCH] 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: + + + + + +
+
+
+ + + + +