diff --git a/packages/main/cypress/specs/Switch.cy.tsx b/packages/main/cypress/specs/Switch.cy.tsx
index 56a65db6f3bb..e7718b4b7519 100644
--- a/packages/main/cypress/specs/Switch.cy.tsx
+++ b/packages/main/cypress/specs/Switch.cy.tsx
@@ -3,6 +3,7 @@ import Label from "../../src/Label.js";
import Switch from "../../src/Switch.js";
describe("General events interactions", () => {
+
it("Should fire change event", () => {
cy.mount(Click me);
@@ -98,6 +99,37 @@ describe("General events interactions", () => {
cy.get("@switch")
.should("not.have.attr", "checked");
});
+
+ it("Should not toggle when readonly (click)", () => {
+ cy.mount();
+
+ cy.get("[ui5-switch]")
+ .as("switch");
+
+ cy.get("@switch")
+ .realClick();
+
+ cy.get("@switch")
+ .should("not.have.attr", "checked");
+ });
+
+ it("Should not toggle when readonly (keyboard)", () => {
+ cy.mount();
+
+ cy.get("[ui5-switch]")
+ .as("switch");
+
+ cy.get("@switch")
+ .shadow()
+ .find(".ui5-switch-root")
+ .focus()
+ .should("be.focused")
+ .realPress("Space");
+
+ cy.get("@switch")
+ .should("not.have.attr", "checked");
+ });
+
});
describe("General accesibility attributes", () => {
@@ -229,6 +261,15 @@ describe("General interactions in form", () => {
});
describe("Accessibility", () => {
+
+ it("should have aria-readonly when readonly", () => {
+ cy.mount();
+ cy.get("[ui5-switch]")
+ .shadow()
+ .find(".ui5-switch-root")
+ .should("have.attr", "aria-readonly", "true");
+ });
+
it("should have correct aria-label when associated with a label via 'for' attribute", () => {
const labelText = "Enable notifications";
diff --git a/packages/main/src/Switch.ts b/packages/main/src/Switch.ts
index 0d45bd6ae52f..1fc420c65266 100644
--- a/packages/main/src/Switch.ts
+++ b/packages/main/src/Switch.ts
@@ -92,11 +92,23 @@ class Switch extends UI5Element implements IFormInputElement {
@property()
design: `${SwitchDesign}` = "Textual";
+ /**
+ * Defines whether the component is in readonly state.
+ *
+ * **Note:** A readonly switch cannot be toggled by user interaction,
+ * but can still be focused and its value read programmatically.
+ * @default false
+ * @public
+ * @since 2.20.0
+ */
+ @property({ type: Boolean })
+ readonly = false;
+
/**
* Defines if the component is checked.
*
* **Note:** The property can be changed with user interaction,
- * either by cliking the component, or by pressing the `Enter` or `Space` key.
+ * either by clicking the component, or by pressing the `Enter` or `Space` key.
* @default false
* @formEvents change
* @formProperty
@@ -232,13 +244,29 @@ class Switch extends UI5Element implements IFormInputElement {
return this.checked ? "accept" : "less";
}
+ _onfocusin() {
+ // Reset keyboard state on focus to prevent stale state from previous interactions
+ this._cancelAction = false;
+ this._isSpacePressed = false;
+ }
+
_onclick() {
+ if (this.readonly) {
+ return;
+ }
this.toggle();
}
_onkeydown(e: KeyboardEvent) {
if (isSpace(e)) {
e.preventDefault();
+ }
+
+ if (this.readonly) {
+ return;
+ }
+
+ if (isSpace(e)) {
this._isSpacePressed = true;
} else if (isShift(e) || isEscape(e)) {
this._cancelAction = true;
@@ -250,6 +278,10 @@ class Switch extends UI5Element implements IFormInputElement {
}
_onkeyup(e: KeyboardEvent) {
+ if (this.readonly) {
+ return;
+ }
+
const isSpaceKey = isSpace(e);
const isCancelKey = isShift(e) || isEscape(e);
@@ -271,7 +303,7 @@ class Switch extends UI5Element implements IFormInputElement {
}
toggle() {
- if (!this.disabled) {
+ if (!this.disabled && !this.readonly) {
this.checked = !this.checked;
const changePrevented = !this.fireDecoratorEvent("change");
// Angular two way data binding;
@@ -303,6 +335,10 @@ class Switch extends UI5Element implements IFormInputElement {
return this.disabled ? undefined : 0;
}
+ get effectiveAriaReadonly() {
+ return this.readonly ? "true" : undefined;
+ }
+
get effectiveAriaDisabled() {
return this.disabled ? "true" : undefined;
}
diff --git a/packages/main/src/SwitchTemplate.tsx b/packages/main/src/SwitchTemplate.tsx
index 01333efe8091..41190999d244 100644
--- a/packages/main/src/SwitchTemplate.tsx
+++ b/packages/main/src/SwitchTemplate.tsx
@@ -20,10 +20,12 @@ export default function SwitchTemplate(this: Switch) {
aria-label={this.ariaLabelText}
aria-checked={this.checked}
aria-disabled={this.effectiveAriaDisabled}
+ aria-readonly={this.effectiveAriaReadonly}
aria-required={this.required}
onClick={this._onclick}
onKeyUp={this._onkeyup}
onKeyDown={this._onkeydown}
+ onFocusIn={this._onfocusin}
tabindex={this.effectiveTabIndex}
title={this.tooltip}
>
diff --git a/packages/main/src/themes/Switch.css b/packages/main/src/themes/Switch.css
index 4a8fc7f2acd7..6801fcc9f021 100644
--- a/packages/main/src/themes/Switch.css
+++ b/packages/main/src/themes/Switch.css
@@ -152,7 +152,7 @@
visibility: var(--_ui5_switch_text_hidden);
}
-.ui5-switch-root.ui5-switch--checked.ui5-switch--semantic .ui5-switch-text--on,
+.ui5-switch-root.ui5-switch--checked.ui5-switch--semantic .ui5-switch-text--on,
.ui5-switch-root.ui5-switch--checked.ui5-switch--desktop.ui5-switch--no-label .ui5-switch-text--on {
inset-inline-start: var(--_ui5_switch_text_active_left);
}
@@ -362,4 +362,50 @@
:dir(rtl).ui5-switch-root.ui5-switch--checked .ui5-switch-slider {
transform: var(--_ui5_switch_rtl_transform);
-}
\ No newline at end of file
+}
+
+/* Readonly switch styling */
+:host([readonly]) .ui5-switch-root {
+ cursor: default;
+}
+
+:host([readonly]) .ui5-switch-track,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-track {
+ background: var(--sapField_ReadOnly_Background);
+ border: 0.0625rem var(--_ui5_switch_readonly_track_border_style) var(--sapField_ReadOnly_BorderColor);
+}
+
+:host([readonly]) .ui5-switch-handle,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-handle {
+ background: var(--sapField_ReadOnly_Background);
+ border: 0.0625rem var(--_ui5_switch_readonly_handle_border_style) var(--sapField_ReadOnly_BorderColor);
+}
+
+:host([readonly]) .ui5-switch-text--on,
+:host([readonly]) .ui5-switch-text--off,
+:host([readonly]) .ui5-switch-no-label-icon-on,
+:host([readonly]) .ui5-switch-no-label-icon-off,
+:host([readonly]) .ui5-switch-icon-on,
+:host([readonly]) .ui5-switch-icon-off,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-icon-on,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-icon-off,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-text--on,
+:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-text--off {
+ color: var(--sapButton_Handle_TextColor);
+}
+
+/* Readonly switch - remove hover effects */
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-handle,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--checked:hover .ui5-switch-handle,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-handle,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic.ui5-switch--checked:hover .ui5-switch-handle {
+ box-shadow: none;
+}
+
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-track,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-handle,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-track,
+:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-handle {
+ background: var(--sapField_ReadOnly_Background);
+ border-color: var(--sapField_ReadOnly_BorderColor);
+}
diff --git a/packages/main/src/themes/base/Switch-parameters.css b/packages/main/src/themes/base/Switch-parameters.css
index b1de7105aff7..6c7a3f649e46 100644
--- a/packages/main/src/themes/base/Switch-parameters.css
+++ b/packages/main/src/themes/base/Switch-parameters.css
@@ -133,6 +133,10 @@
--_ui5_switch_icon_width: 0.75rem;
--_ui5_switch_icon_height: 0.75rem;
+
+ /* readonly - borders */
+ --_ui5_switch_readonly_track_border_style: dashed;
+ --_ui5_switch_readonly_handle_border_style: solid;
}
@container style(--ui5_content_density: compact) {
diff --git a/packages/main/src/themes/sap_horizon_hcb/Switch-parameters.css b/packages/main/src/themes/sap_horizon_hcb/Switch-parameters.css
index 72606539894d..3293e155c4ad 100644
--- a/packages/main/src/themes/sap_horizon_hcb/Switch-parameters.css
+++ b/packages/main/src/themes/sap_horizon_hcb/Switch-parameters.css
@@ -121,6 +121,10 @@
--_ui5_switch_icon_width: 1rem;
--_ui5_switch_icon_height: 1rem;
+
+ /* readonly - solid borders for high contrast */
+ --_ui5_switch_readonly_track_border_style: solid;
+ --_ui5_switch_readonly_handle_border_style: solid;
}
@container style(--ui5_content_density: compact) {
diff --git a/packages/main/src/themes/sap_horizon_hcw/Switch-parameters.css b/packages/main/src/themes/sap_horizon_hcw/Switch-parameters.css
index c2a9324bd411..29c3677006a1 100644
--- a/packages/main/src/themes/sap_horizon_hcw/Switch-parameters.css
+++ b/packages/main/src/themes/sap_horizon_hcw/Switch-parameters.css
@@ -122,6 +122,10 @@
--_ui5_switch_icon_width: 1rem;
--_ui5_switch_icon_height: 1rem;
+
+ /* readonly - solid borders for high contrast */
+ --_ui5_switch_readonly_track_border_style: solid;
+ --_ui5_switch_readonly_handle_border_style: solid;
}
@container style(--ui5_content_density: compact) {
diff --git a/packages/main/test/pages/Switch.html b/packages/main/test/pages/Switch.html
index 036c835a1e96..065790ba534a 100644
--- a/packages/main/test/pages/Switch.html
+++ b/packages/main/test/pages/Switch.html
@@ -42,6 +42,16 @@
Default Switch
+ Readonly Switch
+
+
+
+
+
+
+
+
+
Change prevented Switch