Skip to content

Commit f339d8b

Browse files
authored
Merge pull request #3464 from atmire/item-edit-bitstreams-table-main
Edit Item, Bitstreams tab: Accessibility improvements
2 parents c1aabf6 + cc70eaa commit f339d8b

27 files changed

+2737
-1346
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export class ObjectUpdatesServiceStub {
2+
3+
initialize = jasmine.createSpy('initialize');
4+
saveFieldUpdate = jasmine.createSpy('saveFieldUpdate');
5+
getObjectEntry = jasmine.createSpy('getObjectEntry');
6+
getFieldState = jasmine.createSpy('getFieldState');
7+
getFieldUpdates = jasmine.createSpy('getFieldUpdates');
8+
getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive');
9+
isValid = jasmine.createSpy('isValid');
10+
isValidPage = jasmine.createSpy('isValidPage');
11+
saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate');
12+
saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate');
13+
saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate');
14+
isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata');
15+
setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata');
16+
setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate');
17+
setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate');
18+
discardFieldUpdates = jasmine.createSpy('discardFieldUpdates');
19+
discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates');
20+
reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates');
21+
removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate');
22+
getUpdateFields = jasmine.createSpy('getUpdateFields');
23+
hasUpdates = jasmine.createSpy('hasUpdates');
24+
isReinstatable = jasmine.createSpy('isReinstatable');
25+
getLastModified = jasmine.createSpy('getLastModified');
26+
createPatch = jasmine.createSpy('getPatch');
27+
28+
}

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<div class="item-bitstreams" *ngVar="(bundles$ | async) as bundles">
2+
<div class="mt-2" id="reorder-description">
3+
<ds-alert [content]="'item.edit.bitstreams.info-alert'" [type]="AlertType.Info"></ds-alert>
4+
</div>
5+
26
<div class="button-row top d-flex mt-2 space-children-mr">
37
<button class="mr-auto btn btn-success"
48
[attr.aria-label]="'item.edit.bitstreams.upload-button' | translate"
@@ -27,21 +31,13 @@
2731
</button>
2832
</div>
2933

30-
<div *ngIf="item && bundles?.length > 0" class="container table-bordered mt-4">
31-
<div class="row header-row font-weight-bold">
32-
<div class="{{columnSizes.columns[0].buildClasses()}} row-element">
33-
<ds-item-edit-bitstream-drag-handle></ds-item-edit-bitstream-drag-handle>
34-
{{'item.edit.bitstreams.headers.name' | translate}}
35-
</div>
36-
<div class="{{columnSizes.columns[1].buildClasses()}} row-element">{{'item.edit.bitstreams.headers.description' | translate}}</div>
37-
<div class="{{columnSizes.columns[2].buildClasses()}} text-center row-element">{{'item.edit.bitstreams.headers.format' | translate}}</div>
38-
<div class="{{columnSizes.columns[3].buildClasses()}} text-center row-element">{{'item.edit.bitstreams.headers.actions' | translate}}</div>
39-
</div>
40-
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
34+
<div *ngIf="item && bundles?.length > 0" class="mt-4 table-border scrollable-table" [ngClass]="{'disabled-overlay': (isProcessingMoveRequest | async)}">
35+
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles; first as isFirst"
4136
[bundle]="bundle"
4237
[item]="item"
4338
[columnSizes]="columnSizes"
44-
(dropObject)="dropBitstream(bundle, $event)">
39+
[isFirstTable]="isFirst"
40+
aria-describedby="reorder-description">
4541
</ds-item-edit-bitstream-bundle>
4642
</div>
4743
<div *ngIf="bundles?.length === 0"
@@ -74,3 +70,5 @@
7470
</div>
7571
</div>
7672
</div>
73+
74+
<ds-loading *ngIf="isProcessingMoveRequest | async" class="loading-overlay"></ds-loading>
Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,4 @@
1-
.header-row {
2-
color: var(--bs-table-dark-color);
3-
background-color: var(--bs-table-dark-bg);
4-
border-color: var(--bs-table-dark-border-color);
5-
}
6-
7-
.bundle-row {
8-
color: var(--bs-table-head-color);
9-
background-color: var(--bs-table-head-bg);
10-
border-color: var(--bs-table-border-color);
11-
}
12-
13-
.row-element {
14-
padding: 12px;
15-
padding: 0.75em;
16-
border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color);
17-
}
18-
191
.drag-handle {
20-
visibility: hidden;
212
&:hover {
223
cursor: move;
234
}
@@ -27,10 +8,6 @@
278
cursor: move;
289
}
2910

30-
:host ::ng-deep .bitstream-row:hover .drag-handle, :host ::ng-deep .bitstream-row-drag-handle:focus .drag-handle {
31-
visibility: visible !important;
32-
}
33-
3411
.cdk-drag-preview {
3512
margin-left: 0;
3613
box-sizing: border-box;
@@ -54,3 +31,25 @@
5431
:host ::ng-deep .larger-tooltip .tooltip-inner {
5532
max-width: 500px;
5633
}
34+
35+
.table-border {
36+
border: 1px solid #dee2e6;
37+
}
38+
39+
:host ::ng-deep .pagination {
40+
padding-top: 0.5rem;
41+
}
42+
43+
.scrollable-table {
44+
overflow-x: auto;
45+
}
46+
47+
.disabled-overlay {
48+
opacity: 0.6;
49+
}
50+
51+
.loading-overlay {
52+
position: fixed;
53+
top: 50%;
54+
left: 50%;
55+
}

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts

Lines changed: 130 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TestBed,
88
waitForAsync,
99
} from '@angular/core/testing';
10+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
1011
import {
1112
ActivatedRoute,
1213
Router,
@@ -15,7 +16,6 @@ import { TranslateModule } from '@ngx-translate/core';
1516
import { of as observableOf } from 'rxjs';
1617

1718
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
18-
import { RestResponse } from '../../../core/cache/response.models';
1919
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
2020
import { BundleDataService } from '../../../core/data/bundle-data.service';
2121
import { ItemDataService } from '../../../core/data/item-data.service';
@@ -44,8 +44,12 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
4444
import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe';
4545
import { VarDirective } from '../../../shared/utils/var.directive';
4646
import { ItemBitstreamsComponent } from './item-bitstreams.component';
47+
import { ItemBitstreamsService } from './item-bitstreams.service';
48+
import {
49+
getItemBitstreamsServiceStub,
50+
ItemBitstreamsServiceStub,
51+
} from './item-bitstreams.service.stub';
4752
import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component';
48-
import { ItemEditBitstreamDragHandleComponent } from './item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component';
4953

5054
let comp: ItemBitstreamsComponent;
5155
let fixture: ComponentFixture<ItemBitstreamsComponent>;
@@ -97,6 +101,7 @@ let objectCache: ObjectCacheService;
97101
let requestService: RequestService;
98102
let searchConfig: SearchConfigurationService;
99103
let bundleService: BundleDataService;
104+
let itemBitstreamsService: ItemBitstreamsServiceStub;
100105

101106
describe('ItemBitstreamsComponent', () => {
102107
beforeEach(waitForAsync(() => {
@@ -165,11 +170,19 @@ describe('ItemBitstreamsComponent', () => {
165170
url: url,
166171
});
167172
bundleService = jasmine.createSpyObj('bundleService', {
168-
patch: observableOf(new RestResponse(true, 200, 'OK')),
173+
patch: createSuccessfulRemoteDataObject$({}),
169174
});
170175

176+
itemBitstreamsService = getItemBitstreamsServiceStub();
177+
171178
TestBed.configureTestingModule({
172-
imports: [TranslateModule.forRoot(), ItemBitstreamsComponent, ObjectValuesPipe, VarDirective],
179+
imports: [
180+
TranslateModule.forRoot(),
181+
ItemBitstreamsComponent,
182+
ObjectValuesPipe,
183+
VarDirective,
184+
BrowserAnimationsModule,
185+
],
173186
providers: [
174187
{ provide: ItemDataService, useValue: itemService },
175188
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
@@ -181,6 +194,7 @@ describe('ItemBitstreamsComponent', () => {
181194
{ provide: RequestService, useValue: requestService },
182195
{ provide: SearchConfigurationService, useValue: searchConfig },
183196
{ provide: BundleDataService, useValue: bundleService },
197+
{ provide: ItemBitstreamsService, useValue: itemBitstreamsService },
184198
ChangeDetectorRef,
185199
], schemas: [
186200
NO_ERRORS_SCHEMA,
@@ -189,7 +203,6 @@ describe('ItemBitstreamsComponent', () => {
189203
.overrideComponent(ItemBitstreamsComponent, {
190204
remove: {
191205
imports: [ItemEditBitstreamBundleComponent,
192-
ItemEditBitstreamDragHandleComponent,
193206
ThemedLoadingComponent],
194207
},
195208
})
@@ -209,28 +222,8 @@ describe('ItemBitstreamsComponent', () => {
209222
comp.submit();
210223
});
211224

212-
it('should call removeMultiple on the bitstreamService for the marked field', () => {
213-
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
214-
});
215-
216-
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
217-
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
218-
});
219-
});
220-
221-
describe('when dropBitstream is called', () => {
222-
beforeEach((done) => {
223-
comp.dropBitstream(bundle, {
224-
fromIndex: 0,
225-
toIndex: 50,
226-
finish: () => {
227-
done();
228-
},
229-
});
230-
});
231-
232-
it('should send out a patch for the move operation', () => {
233-
expect(bundleService.patch).toHaveBeenCalled();
225+
it('should call removeMarkedBitstreams on the itemBitstreamsService', () => {
226+
expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled();
234227
});
235228
});
236229

@@ -247,4 +240,114 @@ describe('ItemBitstreamsComponent', () => {
247240
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(bundle.self);
248241
});
249242
});
243+
244+
describe('moveUp', () => {
245+
it('should move the selected bitstream up', () => {
246+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(true);
247+
248+
const event = {
249+
preventDefault: () => {/* Intentionally empty */},
250+
} as KeyboardEvent;
251+
comp.moveUp(event);
252+
253+
expect(itemBitstreamsService.moveSelectedBitstreamUp).toHaveBeenCalled();
254+
});
255+
256+
it('should not do anything if no bitstream is selected', () => {
257+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(false);
258+
259+
const event = {
260+
preventDefault: () => {/* Intentionally empty */},
261+
} as KeyboardEvent;
262+
comp.moveUp(event);
263+
264+
expect(itemBitstreamsService.moveSelectedBitstreamUp).not.toHaveBeenCalled();
265+
});
266+
});
267+
268+
describe('moveDown', () => {
269+
it('should move the selected bitstream down', () => {
270+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(true);
271+
272+
const event = {
273+
preventDefault: () => {/* Intentionally empty */},
274+
} as KeyboardEvent;
275+
comp.moveDown(event);
276+
277+
expect(itemBitstreamsService.moveSelectedBitstreamDown).toHaveBeenCalled();
278+
});
279+
280+
it('should not do anything if no bitstream is selected', () => {
281+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(false);
282+
283+
const event = {
284+
preventDefault: () => {/* Intentionally empty */},
285+
} as KeyboardEvent;
286+
comp.moveDown(event);
287+
288+
expect(itemBitstreamsService.moveSelectedBitstreamDown).not.toHaveBeenCalled();
289+
});
290+
});
291+
292+
describe('cancelSelection', () => {
293+
it('should cancel the selection', () => {
294+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(true);
295+
296+
const event = {
297+
preventDefault: () => {/* Intentionally empty */},
298+
} as KeyboardEvent;
299+
comp.cancelSelection(event);
300+
301+
expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled();
302+
});
303+
304+
it('should not do anything if no bitstream is selected', () => {
305+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(false);
306+
307+
const event = {
308+
preventDefault: () => {/* Intentionally empty */},
309+
} as KeyboardEvent;
310+
comp.cancelSelection(event);
311+
312+
expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled();
313+
});
314+
});
315+
316+
describe('clearSelection', () => {
317+
it('should clear the selection', () => {
318+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(true);
319+
320+
const event = {
321+
target: document.createElement('BODY'),
322+
preventDefault: () => {/* Intentionally empty */},
323+
} as unknown as KeyboardEvent;
324+
comp.clearSelection(event);
325+
326+
expect(itemBitstreamsService.clearSelection).toHaveBeenCalled();
327+
});
328+
329+
it('should not do anything if no bitstream is selected', () => {
330+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(false);
331+
332+
const event = {
333+
target: document.createElement('BODY'),
334+
preventDefault: () => {/* Intentionally empty */},
335+
} as unknown as KeyboardEvent;
336+
comp.clearSelection(event);
337+
338+
expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled();
339+
});
340+
341+
it('should not do anything if the event target is not \'BODY\'', () => {
342+
itemBitstreamsService.hasSelectedBitstream.and.returnValue(true);
343+
344+
const event = {
345+
target: document.createElement('NOT-BODY'),
346+
preventDefault: () => {/* Intentionally empty */},
347+
} as unknown as KeyboardEvent;
348+
comp.clearSelection(event);
349+
350+
expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled();
351+
});
352+
});
250353
});

0 commit comments

Comments
 (0)