Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<ion-item [id]="id"
[ngClass]="{'active': active, 'hasCallToActionBtn': callToActionBtn}"
[lines]="lines"
tabindex="0"
role="listitem">
[button]="button"
[attr.tabindex]="button ? 0 : null"
[attr.role]="itemRole || (button ? 'button' : 'listitem')"
[attr.aria-selected]="ariaSelected === undefined ? null : (ariaSelected ? 'true' : 'false')"
[attr.aria-current]="ariaCurrent || null">
<ng-container *ngIf="!loading; else loadingSkeleton">
<div class="icon-container activitypage" *ngIf="!leadImage">
<ion-icon *ngIf="leadingIcon"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export class ListItemComponent {

// used if there are ending action buttons
@Input() endingActionBtnIcons: string[];
@Input() itemRole: string = 'listitem';
@Input() ariaSelected?: boolean;
@Input() ariaCurrent?: string;
@Input() button = false;
// named as "any" to support any callback parameter format
@Output() anyBtnClick = new EventEmitter<any>();
@Output() actionBtnClick = new EventEmitter<number>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
<ion-segment (ionChange)="switchStatus()" value="pending">
<div class="sr-only" [id]="searchLabelId" i18n>Search reviews</div>
<div class="sr-only" [id]="searchHintId" i18n>Type a review name to filter the list.</div>

<ion-searchbar
class="review-search"
[value]="searchTerm"
(ionInput)="onSearchTermChange($event.detail.value)"
[debounce]="200"
[attr.aria-labelledby]="searchLabelId"
[attr.aria-describedby]="searchHintId"
i18n-placeholder
placeholder="Search reviews"
></ion-searchbar>

<ion-segment
(ionChange)="switchStatus($event)"
[value]="segmentValue"
[attr.aria-controls]="listId"
aria-label="Filter reviews by status" i18n-aria-label
>
<ion-segment-button value="pending" mode="ios">
<ion-label i18n>Pending</ion-label>
</ion-segment-button>
Expand All @@ -7,35 +26,51 @@
</ion-segment-button>
</ion-segment>

<ion-list *ngIf="reviews">
<ng-container *ngFor="let review of reviews">
<h2 class="sr-only" [id]="listLabelId" i18n>Review list</h2>

<ion-list *ngIf="reviews"
[attr.id]="listId"
role="list"
[attr.aria-labelledby]="listLabelId"
>
<ng-container *ngFor="let review of filteredReviews; trackBy: trackBySubmission">
<app-list-item
#reviewItem
class="focusable"
*ngIf="review.isDone === showDone"
[id]="'review-' + review.submissionId"
[title]="review.name"
[subtitle1]="review.submitterName"
subtitle1Color="grey-75"
[subtitle2]="review.teamName"
subtitle2Color="grey-75"
leadingIcon="eye"
[active]="currentReview && currentReview.submissionId === review.submissionId"
[button]="true"
[ariaSelected]="currentReview && currentReview.submissionId === review.submissionId"
[ariaCurrent]="currentReview && currentReview.submissionId === review.submissionId ? 'true' : null"
lines="full"
(click)="goto(review)"
(keydown)="goto(review, $event)"
[endingText]="review.date"
endingTextColor="grey-75"
tabindex="0"
role="button"
></app-list-item>
</ng-container>
</ion-list>

<ion-list *ngIf="reviews === null">
<p *ngIf="resultsAnnouncement" class="sr-only" aria-live="polite">{{ resultsAnnouncement }}</p>

<ion-list *ngIf="reviews === null" role="status" aria-live="polite">
<app-list-item [loading]="true"></app-list-item>
<app-list-item [loading]="true"></app-list-item>
</ion-list>

<div *ngIf="noReviews" class="ion-text-center no-review">
<div *ngIf="hasSearchWithoutResults" class="ion-text-center no-review" role="status" aria-live="polite">
<ion-icon class="large-icon" color="primary" name="search-outline"></ion-icon>
<p class="body-1" i18n>No reviews match your search.</p>
<ion-text class="subtitle-2" color="grey-75" i18n>Try a different search term to find a review.</ion-text>
</div>

<div *ngIf="!hasSearchWithoutResults && noReviews" class="ion-text-center no-review" role="status" aria-live="polite">
<ion-icon class="large-icon" color="primary" name="chatbox-ellipses-outline"></ion-icon>
<p class="body-1" i18n="Sample: You have no pending/completed review yet!">You have no {{ noReviews }} review yet!</p>
<ion-text class="subtitle-2" color="grey-75" i18n>Reviews show up here, so you can easily view them here later.</ion-text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,28 @@ ion-segment {
border: 1px solid var(--ion-color-primary);
}
}
.focusable:focus {
border: 1px solid var(--ion-color-primary);
display: block;

.review-search {
padding: 0;
}
.sr-only {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

.focusable ::ng-deep ion-item:focus-visible {
outline: 2px solid var(--ion-color-primary);
outline-offset: 2px;
}

.focusable ::ng-deep ion-item.active {
border-left: 4px solid var(--ion-color-primary);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';

import { ReviewListComponent } from './review-list.component';
Expand All @@ -10,7 +12,8 @@ describe('ReviewListComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ReviewListComponent ],
imports: [IonicModule.forRoot()]
imports: [IonicModule.forRoot(), FormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();

fixture = TestBed.createComponent(ReviewListComponent);
Expand Down Expand Up @@ -52,15 +55,26 @@ describe('ReviewListComponent', () => {

describe('switchStatus()', () => {
it('should switch status', () => {
component.reviews = [{
isDone: true,
} as any];
component.showDone = false;
component.reviews = [
{ isDone: false, name: 'Pending review', submissionId: 1 } as any,
{ isDone: true, name: 'Completed review', submissionId: 2 } as any,
];
component.currentReview = component.reviews[0];
component.ngOnChanges({
reviews: new SimpleChange(null, component.reviews, true),
currentReview: new SimpleChange(null, component.currentReview, true),
});
component.goToFirstOnSwitch = true;
const spy = spyOn(component.navigate, 'emit');
component.switchStatus();
expect(spy).toHaveBeenCalled();
component.switchStatus({
detail: {
value: 'completed',
},
} as any);
expect(spy).toHaveBeenCalledWith(component.reviews[1]);
expect(component.showDone).toBeTrue();
expect(component.segmentValue).toBe('completed');
expect(component.resultsAnnouncement).toContain('completed');
});
});

Expand All @@ -73,6 +87,7 @@ describe('ReviewListComponent', () => {
component.reviews = [{
isDone: true,
} as any];
component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
expect(component.noReviews).toEqual('');
});

Expand All @@ -81,6 +96,7 @@ describe('ReviewListComponent', () => {
{ isDone: false } as any
];
component.showDone = true;
component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
expect(component.noReviews).toEqual('completed');
});

Expand All @@ -89,7 +105,37 @@ describe('ReviewListComponent', () => {
{ isDone: true } as any
];
component.showDone = false;
component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
expect(component.noReviews).toEqual('pending');
});

it('should hide default message when searching', () => {
component.reviews = [
{ isDone: true, name: 'Completed review', submissionId: 2 } as any,
];
component.showDone = true;
component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
component.onSearchTermChange('');
component.onSearchTermChange('missing');
expect(component.noReviews).toEqual('');
expect(component.hasSearchWithoutResults).toBeTrue();
expect(component.resultsAnnouncement).toContain('No');
});
});

describe('onSearchTermChange()', () => {
it('should filter reviews by title', () => {
component.reviews = [
{ isDone: false, name: 'First review', submissionId: 1 } as any,
{ isDone: false, name: 'Second', submissionId: 2 } as any,
];
component.showDone = false;
component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
component.onSearchTermChange('');
component.onSearchTermChange('second');
expect(component.filteredReviews.length).toBe(1);
expect(component.filteredReviews[0].name).toBe('Second');
expect(component.resultsAnnouncement).toContain('1');
});
});
});
Loading