diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx index 4924b72911157..247421e2a0d53 100644 --- a/packages/main/cypress/specs/Input.cy.tsx +++ b/packages/main/cypress/specs/Input.cy.tsx @@ -3624,16 +3624,21 @@ describe("Input built-in filtering", () => { .should("exist"); }); - it("should sync value-state to slotted input icons", () => { + it("should forward value-state styling to slotted input icons via inherited CSS custom properties", () => { cy.mount( ); + // The parent Input publishes --_ui5_input_icon_state_padding on its host when value-state is set. + // CSS custom properties inherit through the shadow boundary, so the slotted icon reads it inside its own shadow root. cy.get("[ui5-input]") .find("[ui5-input-icon]") - .should("have.attr", "value-state", "Negative"); + .then($icon => { + const computedPadding = getComputedStyle($icon[0]).getPropertyValue("--_ui5_input_icon_state_padding").trim(); + expect(computedPadding).to.not.equal(""); + }); }); it("should work with multiple icons and clear icon", () => { @@ -3670,5 +3675,44 @@ describe("Input built-in filtering", () => { cy.get("@iconClick") .should("have.been.calledOnce"); }); + + it("should forward disabled state to slotted input icons via inherited CSS custom properties", () => { + cy.mount( + + + + ); + + // The parent Input publishes --_ui5_input_icon_state_pointer_events: none on :host([disabled]). + // That inherits into the icon's shadow root and natively blocks hover/active/click. + cy.get("[ui5-input]") + .find("[ui5-input-icon]") + .then($icon => { + const styles = getComputedStyle($icon[0]); + expect(styles.getPropertyValue("--_ui5_input_icon_state_pointer_events").trim()).to.equal("none"); + expect(styles.getPropertyValue("--_ui5_input_icon_state_cursor").trim()).to.equal("default"); + // Opacity inherited as a custom prop, and the icon's shadow root applies it via opacity: var(...) + expect(styles.getPropertyValue("--_ui5_input_icon_state_opacity").trim()).to.not.equal(""); + }); + }); + + it("should forward readonly state to slotted input icons via inherited CSS custom properties", () => { + cy.mount( + + + + ); + + // Readonly parent publishes pointer-events: none + cursor: default (no opacity change). + cy.get("[ui5-input]") + .find("[ui5-input-icon]") + .then($icon => { + const styles = getComputedStyle($icon[0]); + expect(styles.getPropertyValue("--_ui5_input_icon_state_pointer_events").trim()).to.equal("none"); + expect(styles.getPropertyValue("--_ui5_input_icon_state_cursor").trim()).to.equal("default"); + // Readonly does not dim — opacity var should remain unset. + expect(styles.getPropertyValue("--_ui5_input_icon_state_opacity").trim()).to.equal(""); + }); + }); }); }); diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index b7bac5a95cae3..9379a44bc5a4d 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -770,15 +770,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled); this.style.setProperty("--_ui5-input-icons-count", `${this.iconsCount}`); - // Sync value state to slotted input icons - this.icon.forEach((iconElement: IIcon) => { - if (iconElement.hasAttribute("ui5-input-icon")) { - (iconElement as any).valueState = this.valueState; - (iconElement as any).readonly = this.readonly; - (iconElement as any)._parentDisabled = this.disabled; - } - }); - const hasItems = !!this._flattenItems.length; const hasValue = !!this.value; const isFocused = this.shadowRoot!.querySelector("input") === getActiveElement(); diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts index 9340285aac73e..9bb98166af19a 100644 --- a/packages/main/src/InputIcon.ts +++ b/packages/main/src/InputIcon.ts @@ -3,7 +3,6 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement 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"; @@ -63,6 +62,7 @@ class InputIcon extends UI5Element { * Defines the accessible name of the icon. * * **Note:** This property is used for accessibility purposes and will be announced by screen readers. + * When set, it is also rendered as a native `title` tooltip. * * @default undefined * @public @@ -70,50 +70,6 @@ class InputIcon extends UI5Element { @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; - - /** - * Defines whether the icon is readonly. - * - * **Note:** Readonly icons are not interactive and do not fire click events. - * - * @default false - * @public - */ - @property({ type: Boolean }) - readonly = false; - /** * @private */ @@ -126,27 +82,13 @@ class InputIcon extends UI5Element { @property({ type: Boolean, noAttribute: true }) _focused = false; - /** - * @private - */ - @property({ type: Boolean, noAttribute: true }) - _parentDisabled = false; - _onclick(e: MouseEvent) { - if (this.disabled || this.readonly || this._parentDisabled) { - e.stopImmediatePropagation(); - e.preventDefault(); - return; - } - e.stopImmediatePropagation(); this.fireDecoratorEvent("click"); } _onmousedown() { - if (!this.disabled && !this.readonly && !this._parentDisabled) { - this._pressed = true; - } + this._pressed = true; } _onmouseup() { @@ -167,10 +109,6 @@ class InputIcon extends UI5Element { } _onkeydown(e: KeyboardEvent) { - if (this.disabled || this.readonly || this._parentDisabled) { - return; - } - if (isEnter(e) || isSpace(e)) { this._pressed = true; e.preventDefault(); // Prevent scrolling on Space @@ -178,26 +116,18 @@ class InputIcon extends UI5Element { } _onkeyup(e: KeyboardEvent) { - if (this.disabled || this.readonly || this._parentDisabled) { - return; - } - if (isEnter(e) || isSpace(e)) { this._pressed = false; this.fireDecoratorEvent("click"); } } - get effectiveTabIndex() { - return (this.disabled || this.readonly || this._parentDisabled) ? -1 : 0; - } - get effectiveAriaLabel() { return this.accessibleName || undefined; } get effectiveTitle() { - return this.showTooltip && this.accessibleName ? this.accessibleName : undefined; + return this.accessibleName || undefined; } } diff --git a/packages/main/src/InputIconTemplate.tsx b/packages/main/src/InputIconTemplate.tsx index 249fcce58cc7a..20dbd20d812d3 100644 --- a/packages/main/src/InputIconTemplate.tsx +++ b/packages/main/src/InputIconTemplate.tsx @@ -3,40 +3,34 @@ import Icon from "./Icon.js"; export default function InputIconTemplate(this: InputIcon) { return ( - <> - {!this.readonly && -
- {this.name && ( -
- } - +
+ {this.name && ( +
); } diff --git a/packages/main/src/themes/Input.css b/packages/main/src/themes/Input.css index 1e4b79a63087b..c1b61ca559788 100644 --- a/packages/main/src/themes/Input.css +++ b/packages/main/src/themes/Input.css @@ -517,4 +517,64 @@ [ui5-responsive-popover] [ui5-input] { width: 100%; +} + +/* + * Value-state forwarding for slotted . + * + * The parent Input publishes state-specific values via CSS custom properties on its host. + * CSS custom properties inherit through the shadow boundary, so a slotted + * automatically picks them up inside its own shadow root — no JS DOM mutation needed. + */ +:host([value-state="Negative"]), +:host([value-state="Critical"]) { + --_ui5_input_icon_state_padding: var(--_ui5_input_error_warning_icon_padding); +} + +:host([value-state="Information"]) { + --_ui5_input_icon_state_padding: var(--_ui5_input_information_icon_padding); +} + +:host([value-state="Negative"]) { + --_ui5_input_icon_state_pressed_shadow: var(--_ui5_input_error_icon_box_shadow); + --_ui5_input_icon_state_pressed_color: var(--_ui5_input_icon_error_pressed_color); + --_ui5_input_icon_state_focus_outline_color: var(--_ui5_input_focused_value_state_error_focus_outline_color); +} + +:host([value-state="Critical"]) { + --_ui5_input_icon_state_pressed_shadow: var(--_ui5_input_warning_icon_box_shadow); + --_ui5_input_icon_state_pressed_color: var(--_ui5_input_icon_warning_pressed_color); + --_ui5_input_icon_state_focus_outline_color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); +} + +:host([value-state="Information"]) { + --_ui5_input_icon_state_pressed_shadow: var(--_ui5_input_information_icon_box_shadow); + --_ui5_input_icon_state_pressed_color: var(--_ui5_input_icon_information_pressed_color); + --_ui5_input_icon_state_focus_outline_color: var(--sapContent_FocusColor); +} + +:host([value-state="Positive"]) { + --_ui5_input_icon_state_pressed_shadow: var(--_ui5_input_success_icon_box_shadow); + --_ui5_input_icon_state_pressed_color: var(--_ui5_input_icon_success_pressed_color); + --_ui5_input_icon_state_focus_outline_color: var(--_ui5_input_focused_value_state_success_focus_outline_color); +} + +/* + * Disabled forwarding for slotted . Same mechanism: parent publishes vars, + * icon reads them via var() with defaults, no JS DOM mutation. + */ +:host([disabled]) { + --_ui5_input_icon_state_opacity: var(--sapContent_DisabledOpacity); + --_ui5_input_icon_state_pointer_events: none; + --_ui5_input_icon_state_cursor: default; +} + +/* + * Readonly forwarding: the input is still readable but non-editable, so slotted interactive + * icons (search, voice, camera, …) should not fire either. No opacity change — the input + * itself does not dim in readonly. + */ +:host([readonly]:not([disabled])) { + --_ui5_input_icon_state_pointer_events: none; + --_ui5_input_icon_state_cursor: default; } \ No newline at end of file diff --git a/packages/main/src/themes/InputIcon.css b/packages/main/src/themes/InputIcon.css index 876b59e176645..08319972bd94e 100644 --- a/packages/main/src/themes/InputIcon.css +++ b/packages/main/src/themes/InputIcon.css @@ -5,9 +5,14 @@ .inputIcon { color: var(--_ui5_input_icon_color); - cursor: pointer; + /* cursor / opacity / pointer-events are state-aware — parent Input publishes overrides on :host([disabled]). + Defaults preserve the plain interactive appearance. */ + cursor: var(--_ui5_input_icon_state_cursor, pointer); + opacity: var(--_ui5_input_icon_state_opacity, 1); + pointer-events: var(--_ui5_input_icon_state_pointer_events, auto); outline: none; - padding: var(--_ui5_input_icon_padding); + /* padding is state-aware; parent Input publishes the state-specific value via CSS custom property */ + padding: var(--_ui5_input_icon_state_padding, var(--_ui5_input_icon_padding)); border-inline-start: var(--_ui5_input_icon_border); min-width: 1rem; min-height: 1rem; @@ -18,38 +23,33 @@ } .inputIcon:focus-visible { - outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); + outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--_ui5_input_icon_state_focus_outline_color, var(--sapContent_FocusColor)); outline-offset: -0.125rem; } .inputIcon.inputIcon--pressed { background: var(--_ui5_input_icon_pressed_bg); - box-shadow: var(--_ui5_input_icon_box_shadow); + /* box-shadow / color fall back to the plain variant when no value state is set */ + box-shadow: var(--_ui5_input_icon_state_pressed_shadow, var(--_ui5_input_icon_box_shadow)); border-inline-start: var(--_ui5_select_hover_icon_left_border); - color: var(--_ui5_input_icon_pressed_color); + color: var(--_ui5_input_icon_state_pressed_color, var(--_ui5_input_icon_pressed_color)); } -.inputIcon:not(.inputIcon--disabled):active { +.inputIcon:active { background-color: var(--sapButton_Active_Background); - box-shadow: var(--_ui5_input_icon_box_shadow); + box-shadow: var(--_ui5_input_icon_state_pressed_shadow, var(--_ui5_input_icon_box_shadow)); border-inline-start: var(--_ui5_select_hover_icon_left_border); - color: var(--_ui5_input_icon_pressed_color); + color: var(--_ui5_input_icon_state_pressed_color, var(--_ui5_input_icon_pressed_color)); } -.inputIcon:not(.inputIcon--pressed):not(:active):not(.inputIcon--disabled):hover { +.inputIcon:not(.inputIcon--pressed):not(:active):hover { background: var(--_ui5_input_icon_hover_bg); - box-shadow: var(--_ui5_input_icon_box_shadow); + box-shadow: var(--_ui5_input_icon_state_pressed_shadow, var(--_ui5_input_icon_box_shadow)); } -.inputIcon:not(.inputIcon--disabled):hover { +.inputIcon:hover { border-inline-start: var(--_ui5_select_hover_icon_left_border); - box-shadow: var(--_ui5_input_icon_box_shadow); -} - -/* Disabled state styling */ -.inputIcon.inputIcon--disabled { - opacity: var(--sapContent_DisabledOpacity); - cursor: default; + box-shadow: var(--_ui5_input_icon_state_pressed_shadow, var(--_ui5_input_icon_box_shadow)); } @@ -63,73 +63,8 @@ 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:not(.inputIcon--disabled):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):not(.inputIcon--disabled):hover { - box-shadow: var(--_ui5_input_error_icon_box_shadow); -} - -:host([value-state="Critical"]) .inputIcon:not(.inputIcon--disabled):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):not(.inputIcon--disabled):hover { - box-shadow: var(--_ui5_input_warning_icon_box_shadow); -} - -:host([value-state="Information"]) .inputIcon:not(.inputIcon--disabled):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):not(.inputIcon--disabled):hover { - box-shadow: var(--_ui5_input_information_icon_box_shadow); -} - -:host([value-state="Positive"]) .inputIcon:not(.inputIcon--disabled):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):not(.inputIcon--disabled):hover { - box-shadow: var(--_ui5_input_success_icon_box_shadow); -} - -/* Override focus outline colors for value states */ -:host([value-state="Negative"]) .inputIcon.inputIcon--focused { - outline-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); - border-inline-start-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); -} - -:host([value-state="Critical"]) .inputIcon.inputIcon--focused { - outline-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); - border-inline-start-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); -} - -:host([value-state="Information"]) .inputIcon.inputIcon--focused { - outline-color: var(--sapContent_FocusColor); - border-inline-start-color: var(--sapContent_FocusColor); -} - -:host([value-state="Positive"]) .inputIcon.inputIcon--focused { - outline-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); - border-inline-start-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); +/* Focus-outline colour when the parent Input has a value state — driven by inherited CSS custom property */ +.inputIcon.inputIcon--focused { + outline-color: var(--_ui5_input_icon_state_focus_outline_color, var(--sapContent_FocusColor)); + border-inline-start-color: var(--_ui5_input_icon_state_focus_outline_color); } diff --git a/packages/main/test/pages/InputInteractiveIcons.html b/packages/main/test/pages/InputInteractiveIcons.html index c8d6c2e81006e..a29c9071a9cde 100644 --- a/packages/main/test/pages/InputInteractiveIcons.html +++ b/packages/main/test/pages/InputInteractiveIcons.html @@ -180,9 +180,6 @@

With InputIcon (Interactive)

-
- Toggle Icon Disabled -
Negative state: @@ -536,11 +533,7 @@

9. Disabled Inputs with Interactive Icons

}); // Toggle disabled for a single icon - const toggleIconDisabledBtn = document.getElementById("toggle-icon-disabled"); - const toggleIcon = document.getElementById("toggle-icon"); - toggleIconDisabledBtn.addEventListener("click", () => { - toggleIcon.toggleAttribute("disabled"); - }); + // (Removed: ui5-input-icon no longer exposes a `disabled` property.) // Event logging for accessibility demo const eventLog = document.getElementById("event-log-content"); diff --git a/packages/website/docs/_components_pages/main/Input/InputIcon.mdx b/packages/website/docs/_components_pages/main/Input/InputIcon.mdx new file mode 100644 index 0000000000000..fb322caab9073 --- /dev/null +++ b/packages/website/docs/_components_pages/main/Input/InputIcon.mdx @@ -0,0 +1,7 @@ +--- +slug: ../../InputIcon +--- + +<%COMPONENT_OVERVIEW%> + +<%COMPONENT_METADATA%>