From ec2730e5f2ff941facf818952c4a7976e4509acb Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Tue, 9 Jun 2026 17:40:24 +0200 Subject: [PATCH 1/2] feat: fix a11y issues in loader component --- playwright/cps-accessibility.spec.ts | 17 +- .../src/app/api-data/cps-loader.json | 24 +++ .../loader-page/loader-page.component.scss | 11 +- .../cps-button/cps-button.component.spec.ts | 14 +- .../cps-loader/cps-loader.component.html | 13 +- .../cps-loader/cps-loader.component.scss | 30 ++- .../cps-loader/cps-loader.component.spec.ts | 178 +++++++++++++++- .../cps-loader/cps-loader.component.ts | 69 ++++++- .../cps-live-announcer.service.spec.ts | 191 ++++++++++++++++++ .../cps-live-announcer.service.ts | 99 +++++++++ projects/cps-ui-kit/src/public-api.ts | 1 + 11 files changed, 610 insertions(+), 37 deletions(-) create mode 100644 projects/cps-ui-kit/src/lib/services/cps-live-announcer/cps-live-announcer.service.spec.ts create mode 100644 projects/cps-ui-kit/src/lib/services/cps-live-announcer/cps-live-announcer.service.ts diff --git a/playwright/cps-accessibility.spec.ts b/playwright/cps-accessibility.spec.ts index 0647f6d0c..40e50753a 100644 --- a/playwright/cps-accessibility.spec.ts +++ b/playwright/cps-accessibility.spec.ts @@ -118,7 +118,22 @@ const components: ComponentEntry[] = [ // { route: '/icon', name: 'Icon', selector: 'cps-icon' }, { route: '/info-circle', name: 'Info circle', selector: 'cps-info-circle' }, { route: '/input', name: 'Input', selector: '.example-content cps-input' }, - // { route: '/loader', name: 'Loader', selector: 'cps-loader' }, + { route: '/loader', name: 'Loader', selector: '.example-content cps-loader' }, + { + route: '/loader', + name: 'Loader fullscreen', + selector: '.example-content cps-loader', + setup: async (page) => { + await page.waitForSelector('.example-content'); + await page + .locator('.example-content cps-button') + .filter({ hasText: /Toggle fullscreen loader/i }) + .click(); + await page.waitForSelector( + '.cps-loader-overlay[style*="position: fixed"]' + ); + } + }, { route: '/menu', name: 'Menu standard', diff --git a/projects/composition/src/app/api-data/cps-loader.json b/projects/composition/src/app/api-data/cps-loader.json index e4d2c2384..173c03d20 100644 --- a/projects/composition/src/app/api-data/cps-loader.json +++ b/projects/composition/src/app/api-data/cps-loader.json @@ -36,6 +36,30 @@ "type": "boolean", "default": "true", "description": "Determines whether to show 'Loading...' label." + }, + { + "name": "label", + "optional": false, + "readonly": false, + "type": "string", + "default": "Loading...", + "description": "Text shown visually when showLabel is true." + }, + { + "name": "ariaLabel", + "optional": false, + "readonly": false, + "type": "string", + "default": "Loading", + "description": "Text announced by screen readers. Used when showLabel is false or label\nis empty." + }, + { + "name": "doneAriaLabel", + "optional": false, + "readonly": false, + "type": "string", + "default": "Loading complete", + "description": "Text announced by screen readers when the loader is destroyed." } ] } diff --git a/projects/composition/src/app/pages/loader-page/loader-page.component.scss b/projects/composition/src/app/pages/loader-page/loader-page.component.scss index 8dcf78cf1..2180af06f 100644 --- a/projects/composition/src/app/pages/loader-page/loader-page.component.scss +++ b/projects/composition/src/app/pages/loader-page/loader-page.component.scss @@ -1,19 +1,20 @@ $loaders-label-color: var(--cps-color-text-darkest); .loaders-group { - gap: 30px; + gap: 1.875rem; + margin-left: 0.5rem; display: flex; flex-direction: column; &-label { - margin-bottom: 8px; + margin-bottom: 0.5rem; line-height: 2; color: $loaders-label-color; } .loader-container { - width: 450px; - height: 200px; - border: 1px solid lightgrey; + width: 28.125rem; + height: 12.5rem; + border: 0.0625rem solid lightgrey; } } diff --git a/projects/cps-ui-kit/src/lib/components/cps-button/cps-button.component.spec.ts b/projects/cps-ui-kit/src/lib/components/cps-button/cps-button.component.spec.ts index 62969c9ab..211b44074 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-button/cps-button.component.spec.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-button/cps-button.component.spec.ts @@ -281,18 +281,28 @@ describe('CpsButtonComponent', () => { expect(button.getAttribute('type')).toBe('submit'); }); - it('should fall back to "button" native type if nativeType set to null', () => { + it('should fall back to "button" and warn if nativeType set to null', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); fixture.componentRef.setInput('nativeType', null); fixture.detectChanges(); const button = fixture.nativeElement.querySelector('button'); expect(button.getAttribute('type')).toBe('button'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid nativeType value') + ); + warnSpy.mockRestore(); }); - it('should fall back to "button" native type if nativeType set to undefined', () => { + it('should fall back to "button" and warn if nativeType set to undefined', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); fixture.componentRef.setInput('nativeType', undefined); fixture.detectChanges(); const button = fixture.nativeElement.querySelector('button'); expect(button.getAttribute('type')).toBe('button'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid nativeType value') + ); + warnSpy.mockRestore(); }); it('should fall back to "button" and warn if nativeType is an invalid string', () => { diff --git a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.html b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.html index 7a6ce3895..5df7799e4 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.html @@ -5,12 +5,15 @@ position: fullScreen ? 'fixed' : 'relative' }">
- @if (showLabel) { - + @if (showLabel && label.trim()) { + } -
+