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
2 changes: 1 addition & 1 deletion playwright/cps-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const components: ComponentEntry[] = [
name: 'Sidebar menu',
selector: 'cps-sidebar-menu'
},
// { route: '/switch', name: 'Switch', selector: 'cps-switch' },
{ route: '/switch', name: 'Switch', selector: 'cps-switch' },
{
route: '/tab-group',
name: 'Tabs',
Expand Down
12 changes: 10 additions & 2 deletions projects/composition/src/app/api-data/cps-switch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@
"readonly": false,
"type": "string",
"default": "",
"description": "Label of the component."
"description": "Label of the switch component."
},
{
"name": "ariaLabel",
"optional": false,
"readonly": false,
"type": "string",
"default": "",
"description": "Aria label for the switch component, used for accessibility, it takes precedence over label."
},
{
"name": "disabled",
"optional": false,
"readonly": false,
"type": "boolean",
"default": "false",
"description": "Determines whether the component is disabled."
"description": "Determines whether the switch component is disabled."
},
{
"name": "infoTooltip",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
infoTooltip="Provide any information here">
</cps-switch>
<cps-switch label="Basic switch" [value]="true"></cps-switch>
<cps-switch [value]="true"></cps-switch>
<cps-switch ariaLabel="Unlabeled switch" [value]="true"></cps-switch>
<cps-switch
label="Disabled switch checked"
[disabled]="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.switches-group {
gap: 32px;
gap: 2rem;
display: flex;
flex-direction: column;
margin-left: 0.5rem;
}

.sync-val-example {
.sync-val {
margin-top: 16px;
margin-top: 1rem;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="cps-checkbox-container">
<label class="cps-checkbox" [class.cps-checkbox-disabled]="disabled">
<input
#checkboxInput
type="checkbox"
class="cps-checkbox-input"
[disabled]="disabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
OnInit,
Optional,
Output,
Self
Self,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { CpsInfoCircleComponent } from '../cps-info-circle/cps-info-circle.component';
Expand Down Expand Up @@ -114,10 +115,12 @@ export class CpsCheckboxComponent

private _value = false;

@ViewChild('checkboxInput')
checkboxInput!: ElementRef;

constructor(
@Self() @Optional() private _control: NgControl,
@Inject(DOCUMENT) private document: Document,
private _elementRef: ElementRef<HTMLElement>
@Inject(DOCUMENT) private document: Document
) {
if (this._control) {
this._control.valueAccessor = this;
Expand Down Expand Up @@ -175,6 +178,6 @@ export class CpsCheckboxComponent
setDisabledState(_disabled: boolean) {}

focus() {
this._elementRef?.nativeElement?.querySelector('input')?.focus();
this.checkboxInput?.nativeElement?.focus();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<div class="cps-switch-container">
<label class="cps-switch">
<input
#switchInput
type="checkbox"
role="switch"
[disabled]="disabled"
[checked]="value"
(change)="updateValueEvent($event)" />
<span class="cps-switch-slider"></span>
(change)="updateValueEvent($event)"
[attr.aria-label]="ariaLabel || label || null" />
<span class="cps-switch-slider" aria-hidden="true"></span>
</label>
@if (label) {
<span
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@use '../../../../styles/mixins' as *;

:host {
$color-calm: var(--cps-color-calm);
$label-color: var(--cps-color-text-dark);
$disabled-label-color: var(--cps-color-text-light);
$disabled-slider-color: var(--cps-color-text-lightest);
$disabled-label-color: var(--cps-color-text-mild);
$disabled-slider-color: var(--cps-color-text-light);
$transition-time: 0.2s;

min-width: max-content;
Expand All @@ -16,14 +18,18 @@
.cps-switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
width: 3rem;
height: 1.5rem;
}

.cps-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
}

.cps-switch-slider {
Expand All @@ -35,16 +41,16 @@
bottom: 0;
background-color: white;
transition: $transition-time;
border: 1px solid $color-calm;
border-radius: 30px;
border: 0.0625rem solid $color-calm;
border-radius: 1.875rem;
}

.cps-switch-label {
font-size: 16px;
font-size: 1rem;
font-family: 'Source Sans Pro', sans-serif;
font-style: normal;
color: $label-color;
margin-left: 10px;
margin-left: 0.625rem;
cursor: default;
}
.cps-switch-label-disabled {
Expand All @@ -54,11 +60,11 @@
.cps-switch-slider:before {
position: absolute;
content: '';
height: 14px;
width: 14px;
border-radius: 20px;
left: 4px;
bottom: 4px;
height: 0.875rem;
width: 0.875rem;
border-radius: 1.25rem;
left: 0.25rem;
bottom: 0.25rem;
background-color: $color-calm;
transition: $transition-time;
}
Expand All @@ -67,12 +73,12 @@
background-color: $color-calm;
}

input:focus + .cps-switch-slider {
box-shadow: 0 0 1px $color-calm;
.cps-switch:has(input:focus-visible) {
@include focus-ring(0.1875rem, 0.25rem, 1.875rem);
}

input:checked + .cps-switch-slider:before {
transform: translateX(24px);
transform: translateX(1.5rem);
background-color: white;
}

Expand All @@ -90,11 +96,11 @@
}

.cps-switch-info-circle {
margin-left: 8px;
margin-left: 0.5rem;
::ng-deep cps-icon {
i {
width: 14px;
height: 14px;
width: 0.875rem;
height: 0.875rem;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('CpsSwitchComponent', () => {

fixture = TestBed.createComponent(CpsSwitchComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('ariaLabel', 'Toggle switch');
fixture.detectChanges();
});

Expand Down Expand Up @@ -104,14 +105,14 @@ describe('CpsSwitchComponent', () => {

it('should have disabled class when disabled', () => {
fixture.componentRef.setInput('disabled', true);
fixture.componentRef.setInput('label', 'My Switch');
fixture.detectChanges();
const switchLabel =
fixture.nativeElement.querySelector('.cps-switch-label');
if (switchLabel) {
expect(switchLabel.classList.contains('cps-switch-label-disabled')).toBe(
true
);
}
expect(switchLabel).toBeTruthy();
expect(switchLabel.classList.contains('cps-switch-label-disabled')).toBe(
true
);
const input = fixture.nativeElement.querySelector('input');
expect(input.disabled).toBe(true);
});
Expand All @@ -134,4 +135,34 @@ describe('CpsSwitchComponent', () => {
switchEl.click();
expect(component.value).toBe(false);
});

describe('accessibility', () => {
it('should have role="switch" on the input', () => {
const input = fixture.nativeElement.querySelector('input');
expect(input.getAttribute('role')).toBe('switch');
});

it('should set aria-label from ariaLabel input', () => {
fixture.componentRef.setInput('ariaLabel', 'Custom aria');
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
expect(input.getAttribute('aria-label')).toBe('Custom aria');
});

it('should fall back aria-label to label when ariaLabel is not set', () => {
fixture.componentRef.setInput('ariaLabel', '');
fixture.componentRef.setInput('label', 'Enable Feature');
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
expect(input.getAttribute('aria-label')).toBe('Enable Feature');
});

it('should give ariaLabel precedence over label', () => {
fixture.componentRef.setInput('ariaLabel', 'aria wins');
fixture.componentRef.setInput('label', 'label loses');
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
expect(input.getAttribute('aria-label')).toBe('aria wins');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import {
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Optional,
Output,
Self
Self,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { CpsInfoCircleComponent } from '../cps-info-circle/cps-info-circle.component';
import { CpsTooltipPosition } from '../../directives/cps-tooltip/cps-tooltip.directive';
import { logMissingAriaLabelError } from '../../utils/internal/accessibility-utils';

/**
* CpsSwitchComponent is a component used to toggle a boolean value.
Expand All @@ -22,15 +26,23 @@ import { CpsTooltipPosition } from '../../directives/cps-tooltip/cps-tooltip.dir
templateUrl: './cps-switch.component.html',
styleUrls: ['./cps-switch.component.scss']
})
export class CpsSwitchComponent implements ControlValueAccessor {
export class CpsSwitchComponent
implements OnInit, OnChanges, ControlValueAccessor
{
/**
* Label of the component.
* Label of the switch component.
* @group Props
*/
@Input() label = '';

/**
* Determines whether the component is disabled.
* Aria label for the switch component, used for accessibility, it takes precedence over label.
* @group Props
*/
@Input() ariaLabel = '';

/**
* Determines whether the switch component is disabled.
* @group Props
*/
@Input() disabled = false;
Expand Down Expand Up @@ -88,15 +100,23 @@ export class CpsSwitchComponent implements ControlValueAccessor {

private _value = false;

constructor(
@Self() @Optional() private _control: NgControl,
private _elementRef: ElementRef<HTMLElement>
) {
@ViewChild('switchInput')
switchInput!: ElementRef;

constructor(@Self() @Optional() private _control: NgControl) {
if (this._control) {
this._control.valueAccessor = this;
}
}

ngOnInit(): void {
logMissingAriaLabelError('CpsSwitchComponent', this.label, this.ariaLabel);
}

ngOnChanges(): void {
logMissingAriaLabelError('CpsSwitchComponent', this.label, this.ariaLabel);
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (_event: any) => {};

Expand Down Expand Up @@ -131,6 +151,6 @@ export class CpsSwitchComponent implements ControlValueAccessor {
setDisabledState(_disabled: boolean) {}

focus() {
this._elementRef?.nativeElement?.querySelector('input')?.focus();
this.switchInput?.nativeElement?.focus();
}
}
Loading