Skip to content
Open
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
41 changes: 41 additions & 0 deletions packages/main/cypress/specs/Switch.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Switch onChange={cy.stub().as("changed")}>Click me</Switch>);

Expand Down Expand Up @@ -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(<Switch readonly></Switch>);

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(<Switch readonly></Switch>);

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", () => {
Expand Down Expand Up @@ -229,6 +261,15 @@ describe("General interactions in form", () => {
});

describe("Accessibility", () => {

it("should have aria-readonly when readonly", () => {
cy.mount(<Switch readonly></Switch>);
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";

Expand Down
40 changes: 38 additions & 2 deletions packages/main/src/Switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/main/src/SwitchTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
>
Expand Down
50 changes: 48 additions & 2 deletions packages/main/src/themes/Switch.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -362,4 +362,50 @@

:dir(rtl).ui5-switch-root.ui5-switch--checked .ui5-switch-slider {
transform: var(--_ui5_switch_rtl_transform);
}
}

/* 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);
}
4 changes: 4 additions & 0 deletions packages/main/src/themes/base/Switch-parameters.css
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions packages/main/test/pages/Switch.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ <h3>Default Switch</h3>
</div>
<ui5-label id="lbl"></ui5-label>

<h3>Readonly Switch</h3>
<div class="switch2auto">
<ui5-switch id="readonlySwitchOn" readonly checked text-on="On" text-off="Off"></ui5-switch>
<ui5-switch id="readonlySwitchOff" readonly text-on="On" text-off="Off"></ui5-switch>
<ui5-switch id="readonlyCheckedSwitchOn" readonly checked></ui5-switch>
<ui5-switch id="readonlyCheckedSwitchOff" readonly></ui5-switch>
<ui5-switch id="readonlyGraphicalOn" design="Graphical" readonly checked></ui5-switch>
<ui5-switch id="readonlyGraphicalOff" design="Graphical" readonly></ui5-switch>
</div>

<h3>Change prevented Switch</h3>
<div class="switch2auto">
<ui5-switch id="switchprevented" text-on="On" text-off="Off"></ui5-switch>
Expand Down
Loading