+ @Inject(DOCUMENT) private document: Document
) {
if (this._control) {
this._control.valueAccessor = this;
@@ -175,6 +178,6 @@ export class CpsCheckboxComponent
setDisabledState(_disabled: boolean) {}
focus() {
- this._elementRef?.nativeElement?.querySelector('input')?.focus();
+ this.checkboxInput?.nativeElement?.focus();
}
}
diff --git a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.html b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.html
index 909eefa8d..4230b44fa 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.html
+++ b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.html
@@ -1,16 +1,20 @@
@if (label) {
{{ label }}
}
diff --git a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.scss b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.scss
index 07dbb16de..afa31ab29 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.scss
+++ b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.scss
@@ -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;
@@ -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 {
@@ -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 {
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
}
diff --git a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.spec.ts b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.spec.ts
index c47e8fe84..b1d3425df 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.spec.ts
+++ b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.spec.ts
@@ -13,6 +13,7 @@ describe('CpsSwitchComponent', () => {
fixture = TestBed.createComponent(CpsSwitchComponent);
component = fixture.componentInstance;
+ fixture.componentRef.setInput('ariaLabel', 'Toggle switch');
fixture.detectChanges();
});
@@ -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);
});
@@ -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');
+ });
+ });
});
diff --git a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.ts b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.ts
index d25cbddcf..ad9b80937 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.ts
+++ b/projects/cps-ui-kit/src/lib/components/cps-switch/cps-switch.component.ts
@@ -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.
@@ -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;
@@ -88,15 +100,23 @@ export class CpsSwitchComponent implements ControlValueAccessor {
private _value = false;
- constructor(
- @Self() @Optional() private _control: NgControl,
- private _elementRef: ElementRef
- ) {
+ @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) => {};
@@ -131,6 +151,6 @@ export class CpsSwitchComponent implements ControlValueAccessor {
setDisabledState(_disabled: boolean) {}
focus() {
- this._elementRef?.nativeElement?.querySelector('input')?.focus();
+ this.switchInput?.nativeElement?.focus();
}
}