Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions packages/main/cypress/specs/Input.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Input valueState="Negative">
<InputIcon slot="icon" name="search" accessibleName="Search" />
</Input>
);

// 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", () => {
Expand Down Expand Up @@ -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(
<Input disabled>
<InputIcon slot="icon" name="search" accessibleName="Search" />
</Input>
);

// 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(
<Input readonly>
<InputIcon slot="icon" name="search" accessibleName="Search" />
</Input>
);

// 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("");
});
});
});
});
9 changes: 0 additions & 9 deletions packages/main/src/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
76 changes: 3 additions & 73 deletions packages/main/src/InputIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -63,57 +62,14 @@ 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
*/
@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
*/
Expand All @@ -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() {
Expand All @@ -167,37 +109,25 @@ 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
}
}

_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;
}
}

Expand Down
64 changes: 29 additions & 35 deletions packages/main/src/InputIconTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,34 @@ import Icon from "./Icon.js";

export default function InputIconTemplate(this: InputIcon) {
return (
<>
{!this.readonly &&
<div
class={{
"ui5-input-icon-root": true,
"inputIcon": true,
"inputIcon--pressed": this._pressed,
"inputIcon--focused": this._focused,
"inputIcon--disabled": this.disabled,
}}
role="button"
tabindex={this.effectiveTabIndex}
aria-label={this.effectiveAriaLabel}
aria-disabled={this.disabled}
title={this.effectiveTitle}
onClick={this._onclick}
onMouseDown={this._onmousedown}
onMouseUp={this._onmouseup}
onMouseLeave={this._onmouseleave}
onFocus={this._onfocus}
onBlur={this._onblur}
onKeyDown={this._onkeydown}
onKeyUp={this._onkeyup}
part="root"
>
{this.name && (
<Icon
name={this.name}
class="ui5-input-icon-inner"
aria-hidden="true"
/>
)}
</div>
}
</>
<div
class={{
"ui5-input-icon-root": true,
"inputIcon": true,
"inputIcon--pressed": this._pressed,
"inputIcon--focused": this._focused,
}}
role="button"
tabindex={-1}
aria-label={this.effectiveAriaLabel}
title={this.effectiveTitle}
onClick={this._onclick}
onMouseDown={this._onmousedown}
onMouseUp={this._onmouseup}
onMouseLeave={this._onmouseleave}
onFocus={this._onfocus}
onBlur={this._onblur}
onKeyDown={this._onkeydown}
onKeyUp={this._onkeyup}
part="root"
>
{this.name && (
<Icon
name={this.name}
class="ui5-input-icon-inner"
aria-hidden="true"
/>
)}
</div>
);
}
60 changes: 60 additions & 0 deletions packages/main/src/themes/Input.css
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,64 @@

[ui5-responsive-popover] [ui5-input] {
width: 100%;
}

/*
* Value-state forwarding for slotted <ui5-input-icon>.
*
* 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 <ui5-input-icon>
* 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 <ui5-input-icon>. 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;
}
Loading
Loading