mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
116404: Prevent the opening from the modal using mouse interactions from automatically focussing on the first element
(cherry picked from commit 82ed3aadff
)
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||||
import { Component } from '@angular/core';
|
import { Component, DebugElement } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||||
|
import { MenuSection } from '../../shared/menu/menu-section.model';
|
||||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
|
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
|
||||||
@@ -28,14 +30,10 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: {} },
|
{ provide: 'sectionDataProvider', useValue: {} },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
]
|
TestComponent,
|
||||||
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
],
|
||||||
set: {
|
}).compileComponents();
|
||||||
entryComponents: [TestComponent]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -143,6 +141,8 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while inactive)', () => {
|
describe('when spacebar is pressed on section header (while inactive)', () => {
|
||||||
|
let sidebarToggler: DebugElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component, 'toggleSection').and.callThrough();
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
spyOn(menuService, 'toggleActiveSection');
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
@@ -151,15 +151,27 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.space) action used in our component HTML
|
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call toggleSection on the menuService', () => {
|
it('should call toggleSection on the menuService', () => {
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space', key: ' ' }));
|
||||||
|
|
||||||
expect(component.toggleSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
|
||||||
|
it('should not do anything on keydown space', () => {
|
||||||
|
const event: Event = new KeyboardEvent('keydown', { code: 'Space', key: ' ' });
|
||||||
|
spyOn(event, 'preventDefault').and.callThrough();
|
||||||
|
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while active)', () => {
|
describe('when spacebar is pressed on section header (while active)', () => {
|
||||||
@@ -181,6 +193,105 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when enter is pressed on section header (while inactive)', () => {
|
||||||
|
let sidebarToggler: DebugElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
|
||||||
|
it('should not do anything on keydown space', () => {
|
||||||
|
const event: Event = new KeyboardEvent('keydown', { code: 'Enter' });
|
||||||
|
spyOn(event, 'preventDefault').and.callThrough();
|
||||||
|
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when arrow down is pressed on section header', () => {
|
||||||
|
it('should call activateSection', () => {
|
||||||
|
spyOn(component, 'activateSection').and.callThrough();
|
||||||
|
|
||||||
|
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
// dispatch the (keydown.ArrowDown) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown' }));
|
||||||
|
|
||||||
|
expect(component.focusOnFirstChildSection).toBe(true);
|
||||||
|
expect(component.activateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when tab is pressed on section header', () => {
|
||||||
|
it('should call deactivateSection', () => {
|
||||||
|
spyOn(component, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
// dispatch the (keydown.ArrowDown) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
|
||||||
|
|
||||||
|
expect(component.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigateDropdown', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([
|
||||||
|
Object.assign(new MenuSection(), {
|
||||||
|
id: 'subSection1',
|
||||||
|
model: Object.assign(new LinkMenuItemModel(), {
|
||||||
|
type: 'TEST_LINK',
|
||||||
|
}),
|
||||||
|
parentId: component.section.id,
|
||||||
|
}),
|
||||||
|
Object.assign(new MenuSection(), {
|
||||||
|
id: 'subSection2',
|
||||||
|
model: Object.assign(new LinkMenuItemModel(), {
|
||||||
|
type: 'TEST_LINK',
|
||||||
|
}),
|
||||||
|
parentId: component.section.id,
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
component.ngOnInit();
|
||||||
|
flush();
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.focusOnFirstChildSection = true;
|
||||||
|
component.active$.next(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should close the modal on Tab', () => {
|
||||||
|
spyOn(menuService, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
|
||||||
|
firstSubsection.nativeElement.focus();
|
||||||
|
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
|
||||||
|
|
||||||
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal on Escape', () => {
|
||||||
|
spyOn(menuService, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
|
||||||
|
firstSubsection.nativeElement.focus();
|
||||||
|
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape' }));
|
||||||
|
|
||||||
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on smaller, mobile screens', () => {
|
describe('on smaller, mobile screens', () => {
|
||||||
@@ -265,7 +376,9 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
// declare a test component
|
// declare a test component
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-test-cmp',
|
selector: 'ds-test-cmp',
|
||||||
template: ``
|
template: `
|
||||||
|
<a role="menuitem">link</a>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
class TestComponent {
|
class TestComponent {
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
*/
|
*/
|
||||||
mouseEntered = false;
|
mouseEntered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the section was expanded
|
||||||
|
*/
|
||||||
|
focusOnFirstChildSection = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if screen size was small before a resize event
|
* True if screen size was small before a resize event
|
||||||
*/
|
*/
|
||||||
@@ -81,6 +86,7 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
if (active === true) {
|
if (active === true) {
|
||||||
this.addArrowEventListeners = true;
|
this.addArrowEventListeners = true;
|
||||||
} else {
|
} else {
|
||||||
|
this.focusOnFirstChildSection = undefined;
|
||||||
this.unsubscribeFromEventListeners();
|
this.unsubscribeFromEventListeners();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -92,7 +98,7 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
this.dropdownItems.forEach((item: HTMLElement) => {
|
this.dropdownItems.forEach((item: HTMLElement) => {
|
||||||
item.addEventListener('keydown', this.navigateDropdown.bind(this));
|
item.addEventListener('keydown', this.navigateDropdown.bind(this));
|
||||||
});
|
});
|
||||||
if (this.dropdownItems.length > 0) {
|
if (this.focusOnFirstChildSection && this.dropdownItems.length > 0) {
|
||||||
this.dropdownItems.item(0).focus();
|
this.dropdownItems.item(0).focus();
|
||||||
}
|
}
|
||||||
this.addArrowEventListeners = false;
|
this.addArrowEventListeners = false;
|
||||||
@@ -104,6 +110,18 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
this.unsubscribeFromEventListeners();
|
this.unsubscribeFromEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate this section if it's currently inactive, deactivate it when it's currently active.
|
||||||
|
* Also saves whether this toggle was performed by a keyboard event (non-click event) in order to know if thi first
|
||||||
|
* item should be focussed when activating a section.
|
||||||
|
*
|
||||||
|
* @param {Event} event The user event that triggered this method
|
||||||
|
*/
|
||||||
|
override toggleSection(event: Event): void {
|
||||||
|
this.focusOnFirstChildSection = event.type !== 'click';
|
||||||
|
super.toggleSection(event);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the current event listeners on the dropdown items (called when the menu is closed & on component
|
* Removes all the current event listeners on the dropdown items (called when the menu is closed & on component
|
||||||
* destruction)
|
* destruction)
|
||||||
@@ -196,9 +214,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
this.deactivateSection(event, false);
|
this.deactivateSection(event, false);
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
|
this.focusOnFirstChildSection = true;
|
||||||
this.activateSection(event);
|
this.activateSection(event);
|
||||||
break;
|
break;
|
||||||
case 'Space':
|
case 'Space':
|
||||||
|
case 'Enter':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -55,9 +55,11 @@ export class MenuSectionComponent implements OnInit, OnDestroy {
|
|||||||
* Set initial values for instance variables
|
* Set initial values for instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.menuService.isSectionActive(this.menuID, this.section.id).pipe(distinctUntilChanged()).subscribe((isActive: boolean) => {
|
this.subs.push(this.menuService.isSectionActive(this.menuID, this.section.id).pipe(distinctUntilChanged()).subscribe((isActive: boolean) => {
|
||||||
|
if (this.active$.value !== isActive) {
|
||||||
this.active$.next(isActive);
|
this.active$.next(isActive);
|
||||||
});
|
}
|
||||||
|
}));
|
||||||
this.initializeInjectorData();
|
this.initializeInjectorData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user