Skip to content

Commit

Permalink
fix: (CXSPA-8991) set aria-label attribute for navigation component b…
Browse files Browse the repository at this point in the history
  • Loading branch information
uroslates authored Dec 11, 2024
1 parent 75a5d1f commit 25b0cd5
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 13 deletions.
15 changes: 11 additions & 4 deletions feature-libs/user/account/components/login/login.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<ng-container *ngIf="user$ | async as user; else login">
<div class="cx-login-greet" id="greeting">
{{ 'miniLogin.userGreeting' | cxTranslate: { name: user.name } }}
</div>
<cx-page-slot id="account-nav" position="HeaderLinks"></cx-page-slot>
<ng-container *ngIf="greeting$ | async as greeting">
<div class="cx-login-greet">
{{ greeting$ | async }}
</div>
<cx-page-slot
(cxDomChange)="onRootNavBtnAdded($event, greeting)"
cxDomChangeTargetSelector="nav ul li:first-child button"
id="account-nav"
position="HeaderLinks"
></cx-page-slot>
</ng-container>
</ng-container>

<ng-template #login>
Expand Down
41 changes: 38 additions & 3 deletions feature-libs/user/account/components/login/login.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
RoutingService,
User,
} from '@spartacus/core';
import { UserAccountFacade } from '@spartacus/user/account/root';
import { Observable, of } from 'rxjs';
import { LoginComponent } from './login.component';
import { UserAccountFacade } from '@spartacus/user/account/root';
import createSpy = jasmine.createSpy;

const mockUserDetails: User = {
Expand Down Expand Up @@ -40,7 +40,17 @@ class MockUserAccountFacade {

@Component({
selector: 'cx-page-slot',
template: '',
template: `
<cx-navigation-ui>
<nav>
<ul>
<li>
<button>Navigation Trigger</button>
</li>
</ul>
</nav>
</cx-navigation-ui>
`,
})
class MockDynamicSlotComponent {
@Input()
Expand All @@ -54,6 +64,8 @@ class MockUrlPipe implements PipeTransform {
transform(): void {}
}

let expectedGreeting = `miniLogin.userGreeting name:${mockUserDetails.name}`;

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
Expand Down Expand Up @@ -103,6 +115,12 @@ describe('LoginComponent', () => {
expect(user).toEqual(mockUserDetails);
});

it('should have greeting details when token exists', () => {
let greeting;
component.greeting$.subscribe((result) => (greeting = result));
expect(greeting).toEqual(expectedGreeting);
});

it('should not get user details when token is lacking', () => {
spyOn(authService, 'isUserLoggedIn').and.returnValue(of(false));

Expand All @@ -126,7 +144,7 @@ describe('LoginComponent', () => {

it('should display greeting message when the user is logged in', () => {
expect(fixture.debugElement.nativeElement.innerText).toContain(
'miniLogin.userGreeting name:First Last'
expectedGreeting
);
});

Expand All @@ -139,5 +157,22 @@ describe('LoginComponent', () => {
'miniLogin.signInRegister'
);
});

it('should contain the dynamic slot: HeaderLinks', () => {
spyOn(component, 'onRootNavBtnAdded').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
expectedGreeting = 'Testing;';
const expectedRootNavBtn = fixture.debugElement.query(
By.css('cx-navigation-ui nav ul li:first-child button')
);
const mockedMutation = {
target: expectedRootNavBtn.nativeNode,
} as MutationRecord;
expect(expectedRootNavBtn.nativeElement.ariaLabel).toBe(null);
component.onRootNavBtnAdded(mockedMutation, expectedGreeting);
expect(expectedRootNavBtn).not.toBeNull();
expect(expectedRootNavBtn.nativeElement.ariaLabel).toBe(expectedGreeting);
});
});
});
21 changes: 19 additions & 2 deletions feature-libs/user/account/components/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*/

import { Component, OnInit } from '@angular/core';
import { AuthService, useFeatureStyles } from '@spartacus/core';
import {
AuthService,
TranslationService,
useFeatureStyles,
} from '@spartacus/core';
import { User, UserAccountFacade } from '@spartacus/user/account/root';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
Expand All @@ -16,10 +20,12 @@ import { switchMap } from 'rxjs/operators';
})
export class LoginComponent implements OnInit {
user$: Observable<User | undefined>;
greeting$: Observable<string | undefined>;

constructor(
private auth: AuthService,
private userAccount: UserAccountFacade
private userAccount: UserAccountFacade,
private translation: TranslationService
) {
useFeatureStyles('a11yMyAccountLinkOutline');
}
Expand All @@ -34,5 +40,16 @@ export class LoginComponent implements OnInit {
}
})
);
this.greeting$ = this.user$.pipe(
switchMap((user) =>
this.translation.translate(`miniLogin.userGreeting`, {
name: user?.name,
})
)
);
}

onRootNavBtnAdded($event: MutationRecord, greeting: string) {
($event.target as HTMLElement).setAttribute('aria-label', greeting);
}
}
11 changes: 9 additions & 2 deletions feature-libs/user/account/components/login/login.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ import {
provideDefaultConfig,
UrlModule,
} from '@spartacus/core';
import { PageSlotModule } from '@spartacus/storefront';
import { DomChangeModule, PageSlotModule } from '@spartacus/storefront';
import { LoginComponent } from './login.component';

@NgModule({
imports: [CommonModule, RouterModule, UrlModule, PageSlotModule, I18nModule],
imports: [
CommonModule,
RouterModule,
UrlModule,
PageSlotModule,
I18nModule,
DomChangeModule,
],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
Expand Down
3 changes: 2 additions & 1 deletion projects/assets/src/translations/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"goTo": "Go to {{location}}",
"navigateTo": "Navigate to {{nav}}",
"scrollToTop": "Scroll back to the top of the page",
"linkItemInList": "{{title}}. {{position}} of {{listLength}}"
"linkItemInList": "{{title}}. {{position}} of {{listLength}}",
"menuButonTitle": "{{title}} Menu"
},
"searchBox": {
"placeholder": "Enter product name or SKU",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
[attr.aria-haspopup]="true"
[attr.aria-expanded]="false"
[attr.aria-label]="node.title"
[attr.title]="
'navigation.menuButonTitle' | cxTranslate: { title: node.title }
"
(click)="toggleOpen($any($event))"
(mouseenter)="onMouseEnter($event)"
(keydown.space)="toggleOpen($any($event))"
Expand All @@ -108,7 +111,10 @@
[attr.aria-haspopup]="true"
[attr.aria-expanded]="false"
[attr.aria-controls]="node.title"
[attr.aria-describedby]="'greeting'"
[attr.aria-label]="node.title"
[attr.title]="
'navigation.menuButonTitle' | cxTranslate: { title: node.title }
"
(click)="toggleOpen($any($event))"
(mouseenter)="onMouseEnter($event)"
(keydown.space)="onSpace($any($event))"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,4 +463,29 @@ describe('Navigation UI Component', () => {
expect(mockHeader.focus).toHaveBeenCalled();
}));
});

describe('trigger buttions ariaLabel/title', () => {
it('should have the ariaLabel and title set', () => {
const rootNode = mockNode.children?.[0];
const childNode = rootNode?.children?.[0];
const rootTitle = rootNode?.title;
const childTitle = childNode?.title;
fixture.detectChanges();
const nestedTriggerButton = fixture.debugElement.query(
By.css(`button[aria-label="${childTitle}"]`)
).nativeElement;
const rootTriggerButton = fixture.debugElement.query(
By.css(`button[aria-label="${rootTitle}"]`)
).nativeElement;

expect(nestedTriggerButton).toBeDefined();
expect(rootTriggerButton).toBeDefined();
expect(rootTriggerButton.getAttribute('title')).toEqual(
`navigation.menuButonTitle title:${rootTitle}`
);
expect(nestedTriggerButton.getAttribute('title')).toEqual(
`navigation.menuButonTitle title:${childTitle}`
);
});
});
});
1 change: 1 addition & 0 deletions projects/storefrontlib/layout/a11y/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
export * from './keyboard-focus/index';
export * from './skip-link/index';
export * from './btn-like-link';
export * from './on-dom-change';
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Component, ElementRef } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DomChangeDirective } from './dom-change.directive';

@Component({
template: `
<div id="testElement" cxDomChange>
<div class="targetElement"></div>
</div>
`,
})
class TestHostComponent {}

describe('DomChangeDirective', () => {
// let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let testElement: ElementRef;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [TestHostComponent, DomChangeDirective],
imports: [BrowserAnimationsModule],
}).compileComponents();

fixture = TestBed.createComponent(TestHostComponent);

testElement = fixture.debugElement.query(By.directive(DomChangeDirective));
}));

it('should emit when a child element is added', (done) => {
const directive = fixture.debugElement
.query(By.directive(DomChangeDirective))
.injector.get(DomChangeDirective);
const newElement = document.createElement('div');

directive.cxDomChange.subscribe((mutation: MutationRecord) => {
expect(mutation.type).toBe('childList');
done();
});

// Set DOM
testElement.nativeElement.appendChild(newElement);
fixture.detectChanges();
});

it('should emit when a child element is removed', (done) => {
const directive = fixture.debugElement
.query(By.directive(DomChangeDirective))
.injector.get(DomChangeDirective);
const childElement =
testElement.nativeElement.querySelector('.targetElement');

directive.cxDomChange.subscribe((mutation: MutationRecord) => {
expect(mutation.type).toBe('childList');
done();
});

// Set DOM
testElement.nativeElement.removeChild(childElement);
fixture.detectChanges();
});

it('should filter mutations based on the target selector', (done) => {
const directive = fixture.debugElement
.query(By.directive(DomChangeDirective))
.injector.get(DomChangeDirective);
directive.cxDomChangeTargetSelector = '.targetElement';

directive.cxDomChange.subscribe((mutation: MutationRecord) => {
expect(mutation.target).toHaveClass('targetElement');
done();
});

// Set DOM
const targetElement =
testElement.nativeElement.querySelector('.targetElement');
targetElement.appendChild(document.createTextNode('Test Text'));
fixture.detectChanges();
});

it('should not emit when mutations do not match target selector', () => {
let called = false;
const directive = fixture.debugElement
.query(By.directive(DomChangeDirective))
.injector.get(DomChangeDirective);
directive.cxDomChangeTargetSelector = '.non-matching-selector';

directive.cxDomChange.subscribe(() => {
called = true;
});

// Set DOM
const newElement = document.createElement('div');
testElement.nativeElement.appendChild(newElement);
fixture.detectChanges();

expect(called).toBe(false);
});
});
Loading

0 comments on commit 25b0cd5

Please sign in to comment.