mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[ACS-10263] sr personal files mark all as read button incorrectly grouped as notification list (#11331)
* [ACS-10263] Exclude mark all as read button from menu items * [ACS-10263] Keyboard navigation for notifications list * [ACS-10263] Excluded load more button from screen reader list for notifications * [ACS-10263] Excluded non list related elements from screen reading as list for columns visibility * [ACS-10263] Unit tests * [ACS-10263] Added readonly and private for constructor parameters * [ACS-10263] Marked fields as readonly
This commit is contained in:
+9
-11
@@ -13,7 +13,6 @@
|
||||
<button
|
||||
data-automation-id="adf-columns-selector-close-button"
|
||||
mat-icon-button
|
||||
role="menuitem"
|
||||
[attr.aria-label]="'CLOSE' | translate"
|
||||
(click)="closeMenu()"
|
||||
>
|
||||
@@ -34,21 +33,21 @@
|
||||
class="adf-columns-selector-search-input"
|
||||
data-automation-id="adf-columns-selector-search-input"
|
||||
type="text"
|
||||
role="menuitem"
|
||||
[placeholder]='"ADF-DATATABLE.COLUMNS_SELECTOR.SEARCH" | translate'>
|
||||
</div>
|
||||
|
||||
<div class="adf-columns-selector-list-container">
|
||||
<mat-selection-list
|
||||
class="adf-columns-selector-list-container"
|
||||
(selectionChange)="changeColumnVisibility($event)"
|
||||
[attr.aria-label]='"ADF-DATATABLE.COLUMNS_SELECTOR.COLUMNS_VISIBILITY" | translate'>
|
||||
<div *ngFor="let column of (columnItems | columnsSearchFilter: searchQuery)" class="adf-columns-selector-list-item">
|
||||
<mat-checkbox
|
||||
<mat-list-option
|
||||
color="primary"
|
||||
class="adf-columns-selector-column-checkbox"
|
||||
[attr.data-automation-id]="'adf-columns-selector-column-checkbox-' + column.title"
|
||||
[checked]="!column.isHidden"
|
||||
role="menuitem"
|
||||
[selected]="!column.isHidden"
|
||||
[disabled]="isCheckboxDisabled(column)"
|
||||
(keydown.enter)="changeColumnVisibility(column)"
|
||||
(change)="changeColumnVisibility(column)">
|
||||
[value]="column.id">
|
||||
<div class="adf-columns-selector-list-content">
|
||||
{{column.title | translate}}
|
||||
</div>
|
||||
@@ -63,16 +62,15 @@
|
||||
{{column.subtitle | translate}}
|
||||
</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</mat-list-option>
|
||||
</div>
|
||||
</div>
|
||||
</mat-selection-list>
|
||||
|
||||
<mat-divider class="adf-columns-selector-divider" />
|
||||
|
||||
<div class="adf-columns-selector-footer">
|
||||
<button
|
||||
mat-flat-button
|
||||
role="menuitem"
|
||||
data-automation-id="adf-columns-selector-apply-button"
|
||||
color="primary"
|
||||
(click)="apply()">
|
||||
|
||||
+102
-89
@@ -1,3 +1,5 @@
|
||||
@use '../../../styles/mat-selectors' as ms;
|
||||
|
||||
$adf-columns-selector-space: 12px;
|
||||
|
||||
@mixin adf-columns-selector-side-padding {
|
||||
@@ -8,101 +10,112 @@ $adf-columns-selector-space: 12px;
|
||||
padding: $adf-columns-selector-space 0;
|
||||
}
|
||||
|
||||
.adf-columns-selector {
|
||||
@include adf-columns-selector-top-bottom-padding;
|
||||
adf-datatable-column-selector {
|
||||
.adf-columns-selector {
|
||||
@include adf-columns-selector-top-bottom-padding;
|
||||
|
||||
min-width: 277px;
|
||||
min-width: 277px;
|
||||
|
||||
&-header {
|
||||
@include adf-columns-selector-side-padding;
|
||||
&-header {
|
||||
@include adf-columns-selector-side-padding;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-header-label {
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
}
|
||||
|
||||
&-list-container {
|
||||
max-height: 350px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
&-list-item {
|
||||
margin-top: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--adf-theme-background-hover-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-list-content {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
color: var(--adf-theme-foreground-text-color);
|
||||
font-size: var(--theme-caption-font-size);
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
column-gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-subtitle-icon {
|
||||
margin-top: 3px;
|
||||
height: var(--theme-caption-font-size);
|
||||
width: var(--theme-caption-font-size);
|
||||
font-size: var(--theme-caption-font-size);
|
||||
}
|
||||
|
||||
&-column-checkbox {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@include adf-columns-selector-side-padding;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
&-search-input-container {
|
||||
@include adf-columns-selector-side-padding;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&-search-input {
|
||||
padding: 10px 10px 10px 29px;
|
||||
width: 100%;
|
||||
outline: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--theme-background-color);
|
||||
background: var(--theme-background-color);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
&-header-label {
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
}
|
||||
}
|
||||
|
||||
&-search-input-icon {
|
||||
position: absolute;
|
||||
left: 17px;
|
||||
top: 10px;
|
||||
font-size: var(--theme-adf-icon-1-font-size);
|
||||
&-list-container {
|
||||
max-height: 350px;
|
||||
overflow: hidden auto;
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
.adf-columns-selector-list-item {
|
||||
margin-top: 10px;
|
||||
|
||||
#{ms.$mat-list-item} {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
padding-left: 0;
|
||||
height: 24px;
|
||||
|
||||
#{ms.$mat-list-item-end} {
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-list-content {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
width: 210px;
|
||||
color: var(--theme-secondary-text);
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
color: var(--adf-theme-foreground-text-color);
|
||||
font-size: var(--theme-caption-font-size);
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
column-gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-subtitle-icon {
|
||||
margin-top: 3px;
|
||||
height: var(--theme-caption-font-size);
|
||||
width: var(--theme-caption-font-size);
|
||||
font-size: var(--theme-caption-font-size);
|
||||
}
|
||||
|
||||
&-column-checkbox {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@include adf-columns-selector-side-padding;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
&-search-input-container {
|
||||
@include adf-columns-selector-side-padding;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
&-search-input {
|
||||
padding: 10px 10px 10px 29px;
|
||||
width: 100%;
|
||||
outline: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--theme-background-color);
|
||||
background: var(--theme-background-color);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-search-input-icon {
|
||||
position: absolute;
|
||||
left: 17px;
|
||||
top: 10px;
|
||||
font-size: var(--theme-adf-icon-1-font-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-40
@@ -114,14 +114,14 @@ describe('ColumnsSelectorComponent', () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const options = await testingUtils.getAllMatListOptions();
|
||||
const inputColumnsWithTitle = inputColumns.filter((column) => !!column.title);
|
||||
expect(checkboxes.length).toBe(inputColumnsWithTitle.length);
|
||||
expect(options.length).toBe(inputColumnsWithTitle.length);
|
||||
|
||||
for await (const checkbox of checkboxes) {
|
||||
const checkboxLabel = await checkbox.getLabelText();
|
||||
for (const option of options) {
|
||||
const optionLabel = await option.getFullText();
|
||||
|
||||
const inputColumn = inputColumnsWithTitle.find((inputColumnWithTitle) => inputColumnWithTitle.title === checkboxLabel);
|
||||
const inputColumn = inputColumnsWithTitle.find((inputColumnWithTitle) => inputColumnWithTitle.title === optionLabel);
|
||||
expect(inputColumn).toBeTruthy('Should have all columns with title');
|
||||
}
|
||||
});
|
||||
@@ -135,37 +135,37 @@ describe('ColumnsSelectorComponent', () => {
|
||||
tick(400);
|
||||
fixture.detectChanges();
|
||||
|
||||
const columnCheckboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const columnOptions = await testingUtils.getAllMatListOptions();
|
||||
|
||||
expect(columnCheckboxes.length).toBe(1);
|
||||
expect(await columnCheckboxes[0].getLabelText()).toBe(inputColumns[0].title);
|
||||
expect(columnOptions.length).toBe(1);
|
||||
expect(await columnOptions[0].getFullText()).toBe(inputColumns[0].title);
|
||||
}));
|
||||
|
||||
it('should change column visibility', async () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const firstColumnCheckbox = await testingUtils.getMatCheckbox();
|
||||
const checkBoxName = await firstColumnCheckbox.getLabelText();
|
||||
const firstColumnOption = await testingUtils.getMatListOption();
|
||||
const optionName = await firstColumnOption.getFullText();
|
||||
|
||||
const toggledColumnItem = component.columnItems.find((item) => item.title === checkBoxName);
|
||||
expect(toggledColumnItem?.isHidden).toBe(undefined);
|
||||
const toggledColumnItem = component.columnItems.find((item) => item.title === optionName);
|
||||
expect(toggledColumnItem?.isHidden).toBeUndefined();
|
||||
|
||||
await firstColumnCheckbox.toggle();
|
||||
await firstColumnOption.toggle();
|
||||
expect(toggledColumnItem?.isHidden).toBeTrue();
|
||||
});
|
||||
|
||||
describe('checkboxes', () => {
|
||||
describe('list options', () => {
|
||||
it('should have set proper default state', async () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const options = await testingUtils.getAllMatListOptions();
|
||||
|
||||
expect(await checkboxes[0].isChecked()).toBe(true);
|
||||
expect(await checkboxes[1].isChecked()).toBe(true);
|
||||
expect(await checkboxes[2].isChecked()).toBe(true);
|
||||
expect(await checkboxes[3].isChecked()).toBe(false);
|
||||
expect(await options[0].isSelected()).toBeTrue();
|
||||
expect(await options[1].isSelected()).toBeTrue();
|
||||
expect(await options[2].isSelected()).toBeTrue();
|
||||
expect(await options[3].isSelected()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should be disabled when visible columns limit is reached', async () => {
|
||||
@@ -173,12 +173,12 @@ describe('ColumnsSelectorComponent', () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const options = await testingUtils.getAllMatListOptions();
|
||||
|
||||
expect(await checkboxes[0].isDisabled()).toBe(false);
|
||||
expect(await checkboxes[1].isDisabled()).toBe(false);
|
||||
expect(await checkboxes[2].isDisabled()).toBe(false);
|
||||
expect(await checkboxes[3].isDisabled()).toBe(true);
|
||||
expect(await options[0].isDisabled()).toBeFalse();
|
||||
expect(await options[1].isDisabled()).toBeFalse();
|
||||
expect(await options[2].isDisabled()).toBeFalse();
|
||||
expect(await options[3].isDisabled()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,12 +202,9 @@ describe('ColumnsSelectorComponent', () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const labeTextOne = await checkboxes[0].getLabelText();
|
||||
const labeTextTwo = await checkboxes[1].getLabelText();
|
||||
|
||||
expect(labeTextOne).toBe(shownDataColumn.title!);
|
||||
expect(labeTextTwo).toBe(hiddenDataColumn.title!);
|
||||
const options = await testingUtils.getAllMatListOptions();
|
||||
expect(await options[0].getFullText()).toBe(shownDataColumn.title);
|
||||
expect(await options[1].getFullText()).toBe(hiddenDataColumn.title);
|
||||
});
|
||||
|
||||
it('should NOT show hidden columns at the end of the list if sorting is disabled', async () => {
|
||||
@@ -216,12 +213,9 @@ describe('ColumnsSelectorComponent', () => {
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const labeTextOne = await checkboxes[0].getLabelText();
|
||||
const labeTextTwo = await checkboxes[1].getLabelText();
|
||||
|
||||
expect(labeTextOne).toBe(hiddenDataColumn.title!);
|
||||
expect(labeTextTwo).toBe(shownDataColumn.title!);
|
||||
const options = await testingUtils.getAllMatListOptions();
|
||||
expect(await options[0].getFullText()).toBe(hiddenDataColumn.title);
|
||||
expect(await options[1].getFullText()).toBe(shownDataColumn.title);
|
||||
});
|
||||
|
||||
it('should show subtitle', async () => {
|
||||
@@ -238,10 +232,7 @@ describe('ColumnsSelectorComponent', () => {
|
||||
component.columnsSorting = false;
|
||||
menuOpenedTrigger.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkboxes = await testingUtils.getAllMatCheckboxes();
|
||||
const labeTextOne = await checkboxes[0].getLabelText();
|
||||
expect(labeTextOne).toBe(`${column.title} ${column.subtitle}`);
|
||||
expect(await (await testingUtils.getMatListOption()).getFullText()).toBe(`${column.title} ${column.subtitle}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
-3
@@ -28,6 +28,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ColumnsSearchFilterPipe } from './columns-search-filter.pipe';
|
||||
import { MatListOption, MatSelectionList, MatSelectionListChange } from '@angular/material/list';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-datatable-column-selector',
|
||||
@@ -39,7 +40,9 @@ import { ColumnsSearchFilterPipe } from './columns-search-filter.pipe';
|
||||
MatDividerModule,
|
||||
ReactiveFormsModule,
|
||||
MatCheckboxModule,
|
||||
ColumnsSearchFilterPipe
|
||||
ColumnsSearchFilterPipe,
|
||||
MatSelectionList,
|
||||
MatListOption
|
||||
],
|
||||
templateUrl: './columns-selector.component.html',
|
||||
styleUrls: ['./columns-selector.component.scss'],
|
||||
@@ -85,8 +88,8 @@ export class ColumnsSelectorComponent implements OnInit {
|
||||
this.mainMenuTrigger.closeMenu();
|
||||
}
|
||||
|
||||
changeColumnVisibility(dataColumn: DataColumn): void {
|
||||
const selectedColumn = this.columnItems.find((column) => column.id === dataColumn.id);
|
||||
changeColumnVisibility(event: MatSelectionListChange): void {
|
||||
const selectedColumn = this.columnItems.find((column) => column.id === event.options[0].value);
|
||||
selectedColumn.isHidden = !selectedColumn.isHidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -426,7 +426,8 @@
|
||||
"COLUMNS_SELECTOR": {
|
||||
"COLUMNS": "Columns",
|
||||
"SEARCH": "Search",
|
||||
"APPLY": "Apply"
|
||||
"APPLY": "Apply",
|
||||
"COLUMNS_VISIBILITY": "Select columns to display"
|
||||
}
|
||||
},
|
||||
"USER_PROFILE": {
|
||||
|
||||
@@ -19,14 +19,16 @@
|
||||
[yPosition]="menuPositionY"
|
||||
id="adf-notification-history-menu"
|
||||
class="adf-notification-history-menu adf-notification-history-menu-panel">
|
||||
<div class="adf-notification-history-list-header">
|
||||
<div class="adf-notification-history-list-header" aria-hidden="true">
|
||||
<span class="adf-notification-history-menu-title">{{ 'NOTIFICATIONS.TITLE' | translate }}</span>
|
||||
<button mat-menu-item
|
||||
<button mat-icon-button
|
||||
*ngIf="notifications.length"
|
||||
id="adf-notification-history-mark-as-read"
|
||||
data-automation-id="adf-notification-history-mark-as-read"
|
||||
class="adf-notification-history-mark-as-read"
|
||||
title="{{ 'NOTIFICATIONS.MARK_AS_READ' | translate }}"
|
||||
(click)="markAsRead()">
|
||||
(click)="markAsRead()"
|
||||
#markAsReadButton
|
||||
(keydown)="manageFocus($event)">
|
||||
<mat-icon class="adf-notification-history-mark-as-read-icon">done_all</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -38,7 +40,8 @@
|
||||
<button mat-menu-item
|
||||
*ngFor="let notification of paginatedNotifications"
|
||||
(click)="onNotificationClick(notification, $event)"
|
||||
class="adf-notification-history-menu-item">
|
||||
class="adf-notification-history-menu-item"
|
||||
(keydown)="manageFocus($event)">
|
||||
<div class="adf-notification-history-menu-item-content">
|
||||
<div *ngIf="notification.initiator; else no_avatar"
|
||||
[outerHTML]="notification.initiator | usernameInitials : 'adf-notification-initiator-pic'"></div>
|
||||
@@ -67,8 +70,16 @@
|
||||
|
||||
<mat-divider/>
|
||||
|
||||
<div class="adf-notification-history-load-more" *ngIf="hasMoreNotifications()">
|
||||
<button mat-menu-item (click)="loadMore($event)">
|
||||
<div
|
||||
class="adf-notification-history-load-more"
|
||||
*ngIf="hasMoreNotifications()"
|
||||
aria-hidden="true">
|
||||
<button
|
||||
data-automation-id="adf-notification-history-load-more"
|
||||
mat-button
|
||||
(click)="loadMore($event)"
|
||||
#loadMoreButton
|
||||
(keydown)="manageFocus($event)">
|
||||
{{ 'NOTIFICATIONS.LOAD_MORE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { NotificationHistoryComponent } from './notification-history.component';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
@@ -24,6 +24,7 @@ import { NOTIFICATION_TYPE, NotificationModel } from '../models/notification.mod
|
||||
import { UnitTestingUtils } from '../../testing/unit-testing-utils';
|
||||
import { provideCoreAuthTesting } from '../../testing/noop-auth.module';
|
||||
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
|
||||
describe('Notification History Component', () => {
|
||||
let fixture: ComponentFixture<NotificationHistoryComponent>;
|
||||
@@ -46,7 +47,7 @@ describe('Notification History Component', () => {
|
||||
});
|
||||
fixture = TestBed.createComponent(NotificationHistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement, TestbedHarnessEnvironment.loader(fixture));
|
||||
|
||||
storage = TestBed.inject(StorageService);
|
||||
notificationService = TestBed.inject(NotificationService);
|
||||
@@ -63,6 +64,15 @@ describe('Notification History Component', () => {
|
||||
});
|
||||
|
||||
describe('ui ', () => {
|
||||
const getMarkAllAsReadButton = (): HTMLButtonElement =>
|
||||
overlayContainerElement.querySelector<HTMLButtonElement>('[data-automation-id="adf-notification-history-mark-as-read"]');
|
||||
|
||||
const getLoadMoreButton = (): HTMLButtonElement =>
|
||||
overlayContainerElement.querySelector<HTMLButtonElement>('[data-automation-id="adf-notification-history-load-more"]');
|
||||
|
||||
const getNotificationElements = (): NodeListOf<HTMLButtonElement> =>
|
||||
overlayContainerElement.querySelectorAll<HTMLButtonElement>('.adf-notification-history-menu-item');
|
||||
|
||||
it('should empty message be present when there are no notifications in the history', (done) => {
|
||||
openNotification();
|
||||
fixture.detectChanges();
|
||||
@@ -80,8 +90,7 @@ describe('Notification History Component', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.notifications.length).toBe(1);
|
||||
const markAllAsRead = overlayContainerElement.querySelector<HTMLButtonElement>('#adf-notification-history-mark-as-read');
|
||||
markAllAsRead.click();
|
||||
getMarkAllAsReadButton().click();
|
||||
fixture.detectChanges();
|
||||
expect(storage.getItem(NotificationHistoryComponent.NOTIFICATION_STORAGE)).toBeNull();
|
||||
expect(component.notifications.length).toBe(0);
|
||||
@@ -115,8 +124,7 @@ describe('Notification History Component', () => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const notification = overlayContainerElement.querySelector<HTMLButtonElement>('.adf-notification-history-menu-item');
|
||||
notification.click();
|
||||
getNotificationElements()[0].click();
|
||||
expect(callBackSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
@@ -133,9 +141,8 @@ describe('Notification History Component', () => {
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const loadMoreButton = overlayContainerElement.querySelector<HTMLButtonElement>('.adf-notification-history-load-more');
|
||||
expect(component.paginatedNotifications.length).toBe(5);
|
||||
expect(loadMoreButton).toBeDefined();
|
||||
expect(getLoadMoreButton()).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -155,8 +162,7 @@ describe('Notification History Component', () => {
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const notification = overlayContainerElement.querySelector<HTMLButtonElement>('.adf-notification-history-menu-item');
|
||||
expect(notification).toBeDefined();
|
||||
expect(getNotificationElements()[0]).toBeDefined();
|
||||
done();
|
||||
});
|
||||
}, 10000);
|
||||
@@ -173,10 +179,140 @@ describe('Notification History Component', () => {
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const notifications = overlayContainerElement.querySelectorAll('.adf-notification-history-menu-item');
|
||||
expect(notifications.length).toBe(6);
|
||||
expect(getNotificationElements().length).toBe(6);
|
||||
done();
|
||||
});
|
||||
}, 45000);
|
||||
|
||||
describe('focus change', () => {
|
||||
let markAllAsReadButton: HTMLButtonElement;
|
||||
let loadMoreButton: HTMLButtonElement;
|
||||
let notifications: NodeListOf<HTMLButtonElement>;
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(storage, 'getItem').and.returnValue(
|
||||
JSON.stringify([
|
||||
{
|
||||
messages: ['My new message 1']
|
||||
},
|
||||
{
|
||||
messages: ['My new message 2']
|
||||
},
|
||||
{
|
||||
messages: ['My new message 3']
|
||||
},
|
||||
{
|
||||
messages: ['My new message 4']
|
||||
},
|
||||
{
|
||||
messages: ['My new message 5']
|
||||
},
|
||||
{
|
||||
messages: ['My new message 6']
|
||||
}
|
||||
] as NotificationModel[])
|
||||
);
|
||||
openNotification();
|
||||
tick();
|
||||
markAllAsReadButton = getMarkAllAsReadButton();
|
||||
loadMoreButton = getLoadMoreButton();
|
||||
notifications = getNotificationElements();
|
||||
}));
|
||||
|
||||
it('should focus mark all as read button when menu has been opened', () => {
|
||||
expect(document.activeElement).toBe(markAllAsReadButton);
|
||||
});
|
||||
|
||||
it('should focus load more button when pressing arrow up when mark all as read button is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(loadMoreButton);
|
||||
});
|
||||
|
||||
it('should focus first notification when pressing arrow down when mark all as read button is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(notifications[0]);
|
||||
});
|
||||
|
||||
it('should focus mark all as read button when pressing arrow up when first notification is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown'
|
||||
})
|
||||
);
|
||||
|
||||
notifications[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(markAllAsReadButton);
|
||||
});
|
||||
|
||||
it('should focus second notification when pressing arrow down when first notification is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown'
|
||||
})
|
||||
);
|
||||
|
||||
notifications[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(notifications[1]);
|
||||
});
|
||||
|
||||
it('should focus mark all as read button when pressing arrow down when load more button is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
|
||||
loadMoreButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(markAllAsReadButton);
|
||||
});
|
||||
|
||||
it('should last paged notification when pressing arrow up when load more button is focused', () => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
|
||||
loadMoreButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
expect(document.activeElement).toBe(notifications[4]);
|
||||
});
|
||||
|
||||
it('should focus correct notification after loading more', fakeAsync(() => {
|
||||
markAllAsReadButton.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowUp'
|
||||
})
|
||||
);
|
||||
|
||||
loadMoreButton.click();
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(document.activeElement).toBe(getNotificationElements()[5]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,13 +15,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { NOTIFICATION_TYPE, NotificationModel } from '../models/notification.model';
|
||||
import { MatMenuModule, MatMenuTrigger, MenuPositionX, MenuPositionY } from '@angular/material/menu';
|
||||
import { MatMenuItem, MatMenuModule, MatMenuTrigger, MenuPositionX, MenuPositionY } from '@angular/material/menu';
|
||||
import { StorageService } from '../../common/services/storage.service';
|
||||
import { PaginationModel } from '../../models/pagination.model';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButton, MatButtonModule, MatIconButton } from '@angular/material/button';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
@@ -30,6 +42,7 @@ import { NgForOf, NgIf } from '@angular/common';
|
||||
import { InitialUsernamePipe, TimeAgoPipe } from '../../pipes';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FocusKeyManager } from '@angular/cdk/a11y';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-notification-history',
|
||||
@@ -73,9 +86,24 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit {
|
||||
paginatedNotifications: NotificationModel[] = [];
|
||||
pagination: PaginationModel;
|
||||
|
||||
@ViewChild('markAsReadButton')
|
||||
private readonly markAsReadButton: MatIconButton;
|
||||
|
||||
@ViewChild('loadMoreButton')
|
||||
private readonly loadMoreButton: MatButton;
|
||||
|
||||
@ViewChildren(MatMenuItem)
|
||||
private readonly menuItems: QueryList<MatMenuItem>;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(private notificationService: NotificationService, public storageService: StorageService, public cd: ChangeDetectorRef) {}
|
||||
private focusKeyManager: FocusKeyManager<MatIconButton | MatMenuItem | MatButton>;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly cd: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.notifications = JSON.parse(this.storageService.getItem(NotificationHistoryComponent.NOTIFICATION_STORAGE)) || [];
|
||||
@@ -108,6 +136,7 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit {
|
||||
|
||||
onMenuOpened() {
|
||||
this.createPagination();
|
||||
setTimeout(() => this.initializeFocusManager(0));
|
||||
}
|
||||
|
||||
markAsRead() {
|
||||
@@ -132,6 +161,7 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit {
|
||||
this.pagination.skipCount = this.pagination.maxItems + this.pagination.skipCount;
|
||||
this.pagination.hasMoreItems = this.notifications.length > this.pagination.skipCount;
|
||||
this.paginatedNotifications = this.notifications.slice(0, this.pagination.skipCount);
|
||||
setTimeout(() => this.initializeFocusManager(this.focusKeyManager.activeItemIndex));
|
||||
}
|
||||
|
||||
hasMoreNotifications(): boolean {
|
||||
@@ -145,4 +175,23 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit {
|
||||
this.trigger.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
manageFocus(event: KeyboardEvent): void {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.focusKeyManager.setPreviousItemActive();
|
||||
event.stopPropagation();
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.stopPropagation();
|
||||
this.focusKeyManager.setNextItemActive();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeFocusManager(activeIndex: number): void {
|
||||
this.focusKeyManager = new FocusKeyManager([
|
||||
this.markAsReadButton,
|
||||
...this.menuItems,
|
||||
...(this.hasMoreNotifications() ? [this.loadMoreButton] : [])
|
||||
]).withWrap();
|
||||
this.focusKeyManager.setActiveItem(activeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ $mat-calendar-table-header: '.mat-calendar-table-header';
|
||||
$mat-calendar-body-disabled: '.mat-calendar-body-disabled';
|
||||
$mat-toolbar: '.mat-toolbar';
|
||||
$mat-list-item-unscoped-content: '.mat-mdc-list-item-unscoped-content';
|
||||
$mat-list-item: '.mat-mdc-list-item';
|
||||
$mat-list-item-end: '.mdc-list-item__end';
|
||||
$mat-text-field-no-label: '.mdc-text-field--no-label';
|
||||
$mat-form-field-infix: '.mat-mdc-form-field-infix';
|
||||
$mat-form-field-error-wrapper: '.mat-mdc-form-field-error-wrapper';
|
||||
|
||||
@@ -32,6 +32,7 @@ import { MatTabGroupHarness, MatTabHarness } from '@angular/material/tabs/testin
|
||||
import { MatToolbarHarness } from '@angular/material/toolbar/testing';
|
||||
import { MatSnackBarHarness } from '@angular/material/snack-bar/testing';
|
||||
import { MatProgressBarHarness } from '@angular/material/progress-bar/testing';
|
||||
import { MatListOptionHarness } from '@angular/material/list/testing';
|
||||
|
||||
export class UnitTestingUtils {
|
||||
constructor(
|
||||
@@ -434,4 +435,14 @@ export class UnitTestingUtils {
|
||||
const progress = await this.loader.getHarness(MatProgressBarHarness);
|
||||
return progress.host();
|
||||
}
|
||||
|
||||
/** MatListOption related methods */
|
||||
|
||||
async getMatListOption(): Promise<MatListOptionHarness> {
|
||||
return this.loader.getHarness(MatListOptionHarness);
|
||||
}
|
||||
|
||||
async getAllMatListOptions(): Promise<MatListOptionHarness[]> {
|
||||
return this.loader.getAllHarnesses(MatListOptionHarness);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user