Skip to content

Commit a594e07

Browse files
authored
feat(ui5-switch): implement readonly state (#13142)
This PR implements `readonly` state of `ui5-switch`. In that state the component is non-interactive, but it is not disabled, and has different styling, as it can be seen below: Default state: <img width="725" height="98" alt="image" src="https://github.com/user-attachments/assets/bbc29701-f709-4f07-bd2e-f2028cdd7ab0" /> Readonly state (all Horizon Themes): <img width="700" height="105" alt="image" src="https://github.com/user-attachments/assets/7fc0c1e2-661b-4cdc-9e09-c83a210b835c" /> <img width="710" height="116" alt="image" src="https://github.com/user-attachments/assets/23a66ab0-98b1-4259-a2b9-f6fa9b0c903a" /> <img width="709" height="98" alt="image" src="https://github.com/user-attachments/assets/9d243f3d-b835-4eed-96aa-3d3717a9392b" /> <img width="722" height="95" alt="image" src="https://github.com/user-attachments/assets/b293a725-9c85-4ec2-b1d1-0ebdefdbfb0e" /> JIRA: BGSOFUIBALKAN-10097
1 parent 476d568 commit a594e07

8 files changed

Lines changed: 151 additions & 4 deletions

File tree

packages/main/cypress/specs/Switch.cy.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Label from "../../src/Label.js";
33
import Switch from "../../src/Switch.js";
44

55
describe("General events interactions", () => {
6+
67
it("Should fire change event", () => {
78
cy.mount(<Switch onChange={cy.stub().as("changed")}>Click me</Switch>);
89

@@ -98,6 +99,37 @@ describe("General events interactions", () => {
9899
cy.get("@switch")
99100
.should("not.have.attr", "checked");
100101
});
102+
103+
it("Should not toggle when readonly (click)", () => {
104+
cy.mount(<Switch readonly></Switch>);
105+
106+
cy.get("[ui5-switch]")
107+
.as("switch");
108+
109+
cy.get("@switch")
110+
.realClick();
111+
112+
cy.get("@switch")
113+
.should("not.have.attr", "checked");
114+
});
115+
116+
it("Should not toggle when readonly (keyboard)", () => {
117+
cy.mount(<Switch readonly></Switch>);
118+
119+
cy.get("[ui5-switch]")
120+
.as("switch");
121+
122+
cy.get("@switch")
123+
.shadow()
124+
.find(".ui5-switch-root")
125+
.focus()
126+
.should("be.focused")
127+
.realPress("Space");
128+
129+
cy.get("@switch")
130+
.should("not.have.attr", "checked");
131+
});
132+
101133
});
102134

103135
describe("General accesibility attributes", () => {
@@ -231,6 +263,15 @@ describe("General interactions in form", () => {
231263
});
232264

233265
describe("Accessibility", () => {
266+
267+
it("should have aria-readonly when readonly", () => {
268+
cy.mount(<Switch readonly></Switch>);
269+
cy.get("[ui5-switch]")
270+
.shadow()
271+
.find(".ui5-switch-root")
272+
.should("have.attr", "aria-readonly", "true");
273+
});
274+
234275
it("should have correct aria-label when associated with a label via 'for' attribute", () => {
235276
const labelText = "Enable notifications";
236277

packages/main/src/Switch.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,23 @@ class Switch extends UI5Element implements IFormInputElement {
9696
@property()
9797
design: `${SwitchDesign}` = "Textual";
9898

99+
/**
100+
* Defines whether the component is in readonly state.
101+
*
102+
* **Note:** A readonly switch cannot be toggled by user interaction,
103+
* but can still be focused and its value read programmatically.
104+
* @default false
105+
* @public
106+
* @since 2.21.0
107+
*/
108+
@property({ type: Boolean })
109+
readonly = false;
110+
99111
/**
100112
* Defines if the component is checked.
101113
*
102114
* **Note:** The property can be changed with user interaction,
103-
* either by cliking the component, or by pressing the `Enter` or `Space` key.
115+
* either by clicking the component, or by pressing the `Enter` or `Space` key.
104116
* @default false
105117
* @formEvents change
106118
* @formProperty
@@ -236,13 +248,29 @@ class Switch extends UI5Element implements IFormInputElement {
236248
return this.checked ? "accept" : "less";
237249
}
238250

251+
_onfocusin() {
252+
// Reset keyboard state on focus to prevent stale state from previous interactions
253+
this._cancelAction = false;
254+
this._isSpacePressed = false;
255+
}
256+
239257
_onclick() {
258+
if (this.readonly) {
259+
return;
260+
}
240261
this.toggle();
241262
}
242263

243264
_onkeydown(e: KeyboardEvent) {
244265
if (isSpace(e)) {
245266
e.preventDefault();
267+
}
268+
269+
if (this.readonly) {
270+
return;
271+
}
272+
273+
if (isSpace(e)) {
246274
this._isSpacePressed = true;
247275
} else if (isShift(e) || isEscape(e)) {
248276
this._cancelAction = true;
@@ -254,6 +282,10 @@ class Switch extends UI5Element implements IFormInputElement {
254282
}
255283

256284
_onkeyup(e: KeyboardEvent) {
285+
if (this.readonly) {
286+
return;
287+
}
288+
257289
const isSpaceKey = isSpace(e);
258290
const isCancelKey = isShift(e) || isEscape(e);
259291

@@ -275,7 +307,7 @@ class Switch extends UI5Element implements IFormInputElement {
275307
}
276308

277309
toggle() {
278-
if (!this.disabled) {
310+
if (!this.disabled && !this.readonly) {
279311
this.checked = !this.checked;
280312
const changePrevented = !this.fireDecoratorEvent("change");
281313
// Angular two way data binding;
@@ -321,6 +353,10 @@ class Switch extends UI5Element implements IFormInputElement {
321353
return this.disabled ? undefined : 0;
322354
}
323355

356+
get effectiveAriaReadonly() {
357+
return this.readonly ? "true" : undefined;
358+
}
359+
324360
get effectiveAriaDisabled() {
325361
return this.disabled ? "true" : undefined;
326362
}

packages/main/src/SwitchTemplate.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ export default function SwitchTemplate(this: Switch) {
2020
aria-label={this.ariaLabelText}
2121
aria-checked={this.checked}
2222
aria-disabled={this.effectiveAriaDisabled}
23+
aria-readonly={this.effectiveAriaReadonly}
2324
aria-required={this.required}
2425
onClick={this._onclick}
2526
onKeyUp={this._onkeyup}
2627
onKeyDown={this._onkeydown}
28+
onFocusIn={this._onfocusin}
2729
tabindex={this.effectiveTabIndex}
2830
title={this.tooltip}
2931
>

packages/main/src/themes/Switch.css

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
visibility: var(--_ui5_switch_text_hidden);
153153
}
154154

155-
.ui5-switch-root.ui5-switch--checked.ui5-switch--semantic .ui5-switch-text--on,
155+
.ui5-switch-root.ui5-switch--checked.ui5-switch--semantic .ui5-switch-text--on,
156156
.ui5-switch-root.ui5-switch--checked.ui5-switch--desktop.ui5-switch--no-label .ui5-switch-text--on {
157157
inset-inline-start: var(--_ui5_switch_text_active_left);
158158
}
@@ -362,4 +362,50 @@
362362

363363
:dir(rtl).ui5-switch-root.ui5-switch--checked .ui5-switch-slider {
364364
transform: var(--_ui5_switch_rtl_transform);
365-
}
365+
}
366+
367+
/* Readonly switch styling */
368+
:host([readonly]) .ui5-switch-root {
369+
cursor: default;
370+
}
371+
372+
:host([readonly]) .ui5-switch-track,
373+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-track {
374+
background: var(--sapField_ReadOnly_Background);
375+
border: 0.0625rem var(--_ui5_switch_readonly_track_border_style) var(--sapField_ReadOnly_BorderColor);
376+
}
377+
378+
:host([readonly]) .ui5-switch-handle,
379+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-handle {
380+
background: var(--sapField_ReadOnly_Background);
381+
border: 0.0625rem var(--_ui5_switch_readonly_handle_border_style) var(--sapField_ReadOnly_BorderColor);
382+
}
383+
384+
:host([readonly]) .ui5-switch-text--on,
385+
:host([readonly]) .ui5-switch-text--off,
386+
:host([readonly]) .ui5-switch-no-label-icon-on,
387+
:host([readonly]) .ui5-switch-no-label-icon-off,
388+
:host([readonly]) .ui5-switch-icon-on,
389+
:host([readonly]) .ui5-switch-icon-off,
390+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-icon-on,
391+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-icon-off,
392+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-text--on,
393+
:host([readonly]) .ui5-switch-root.ui5-switch--semantic .ui5-switch-text--off {
394+
color: var(--sapButton_Handle_TextColor);
395+
}
396+
397+
/* Readonly switch - remove hover effects */
398+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-handle,
399+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--checked:hover .ui5-switch-handle,
400+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-handle,
401+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic.ui5-switch--checked:hover .ui5-switch-handle {
402+
box-shadow: none;
403+
}
404+
405+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-track,
406+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root:hover .ui5-switch-handle,
407+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-track,
408+
:host([readonly]) .ui5-switch--desktop.ui5-switch-root.ui5-switch--semantic:hover .ui5-switch-handle {
409+
background: var(--sapField_ReadOnly_Background);
410+
border-color: var(--sapField_ReadOnly_BorderColor);
411+
}

packages/main/src/themes/base/Switch-parameters.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@
133133

134134
--_ui5_switch_icon_width: 0.75rem;
135135
--_ui5_switch_icon_height: 0.75rem;
136+
137+
/* readonly - borders */
138+
--_ui5_switch_readonly_track_border_style: dashed;
139+
--_ui5_switch_readonly_handle_border_style: solid;
136140
}
137141

138142
@container style(--ui5_content_density: compact) {

packages/main/src/themes/sap_horizon_hcb/Switch-parameters.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121

122122
--_ui5_switch_icon_width: 1rem;
123123
--_ui5_switch_icon_height: 1rem;
124+
125+
/* readonly - solid borders for high contrast */
126+
--_ui5_switch_readonly_track_border_style: solid;
127+
--_ui5_switch_readonly_handle_border_style: solid;
124128
}
125129

126130
@container style(--ui5_content_density: compact) {

packages/main/src/themes/sap_horizon_hcw/Switch-parameters.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@
122122

123123
--_ui5_switch_icon_width: 1rem;
124124
--_ui5_switch_icon_height: 1rem;
125+
126+
/* readonly - solid borders for high contrast */
127+
--_ui5_switch_readonly_track_border_style: solid;
128+
--_ui5_switch_readonly_handle_border_style: solid;
125129
}
126130

127131
@container style(--ui5_content_density: compact) {

packages/main/test/pages/Switch.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ <h3>Default Switch</h3>
4242
</div>
4343
<ui5-label id="lbl"></ui5-label>
4444

45+
<h3>Readonly Switch</h3>
46+
<div class="switch2auto">
47+
<ui5-switch id="readonlySwitchOn" readonly checked text-on="On" text-off="Off"></ui5-switch>
48+
<ui5-switch id="readonlySwitchOff" readonly text-on="On" text-off="Off"></ui5-switch>
49+
<ui5-switch id="readonlyCheckedSwitchOn" readonly checked></ui5-switch>
50+
<ui5-switch id="readonlyCheckedSwitchOff" readonly></ui5-switch>
51+
<ui5-switch id="readonlyGraphicalOn" design="Graphical" readonly checked></ui5-switch>
52+
<ui5-switch id="readonlyGraphicalOff" design="Graphical" readonly></ui5-switch>
53+
</div>
54+
4555
<h3>Change prevented Switch</h3>
4656
<div class="switch2auto">
4757
<ui5-switch id="switchprevented" text-on="On" text-off="Off"></ui5-switch>

0 commit comments

Comments
 (0)