[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:
AleksanderSklorz
2025-11-07 11:26:41 +01:00
committed by GitHub
parent 43622266ce
commit 61e1395bfa
10 changed files with 382 additions and 167 deletions
@@ -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()">
@@ -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);
}
}
}
@@ -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}`);
});
});
});
@@ -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;
}
+2 -1
View File
@@ -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);
}
}