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