Skip to content

Commit 64c6c19

Browse files
authored
Merge pull request #3927 from tdonohue/port_3372_to_8x
[Port dspace-8_x] Make submission reorder buttons keyboard accessible
2 parents 4d41d5f + 0878eaf commit 64c6c19

File tree

5 files changed

+342
-13
lines changed

5 files changed

+342
-13
lines changed

src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
[ngClass]="getClass('element', 'control')">
66

77
<!-- Draggable Container -->
8-
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
8+
<div role="listbox" [attr.aria-label]="'dynamic-form-array.sortable-list.label' | translate" #dropList cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
99
<!-- Draggable Items -->
10-
<div *ngFor="let groupModel of model.groups"
10+
<div #sortableElement
11+
*ngFor="let groupModel of model.groups; let idx = index; let length = count"
1112
role="group"
1213
[formGroupName]="groupModel.index"
1314
[ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"
@@ -16,7 +17,14 @@
1617
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'"
1718
[class.grey-background]="model.isInlineGroupArray">
1819
<!-- Item content -->
19-
<div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0" cdkDragHandle>
20+
<div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0" cdkDragHandle
21+
(focus)="addInstructionMessageToLiveRegion(sortableElement)"
22+
(keydown.space)="toggleKeyboardDragAndDrop($event, sortableElement, idx, length)"
23+
(keydown.enter)="toggleKeyboardDragAndDrop($event, sortableElement, idx, length)"
24+
(keydown.tab)="stopKeyboardDragAndDrop(sortableElement, idx, length)"
25+
(keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)"
26+
(keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')"
27+
(keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')">
2028
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
2129
</div>
2230
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>

src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
margin-right: calc(-0.5 * var(--bs-spacer));
1818
padding-right: calc(0.5 * var(--bs-spacer));
1919
.drag-icon {
20-
visibility: hidden;
2120
width: calc(2 * var(--bs-spacer));
2221
color: var(--bs-gray-600);
2322
margin: var(--bs-btn-padding-y) 0;
@@ -27,9 +26,6 @@
2726

2827
&:hover, &:focus {
2928
cursor: grab;
30-
.drag-icon {
31-
visibility: visible;
32-
}
3329
}
3430

3531
}
@@ -40,18 +36,12 @@
4036
}
4137

4238
&:focus {
43-
.drag-icon {
44-
visibility: visible;
45-
}
4639
}
4740
}
4841

4942
.cdk-drop-list-dragging {
5043
.drag-handle {
5144
cursor: grabbing;
52-
.drag-icon {
53-
visibility: hidden;
54-
}
5545
}
5646
}
5747

@@ -63,3 +53,9 @@
6353
.cdk-drag-placeholder {
6454
opacity: 0;
6555
}
56+
57+
::ng-deep {
58+
.sorting-with-keyboard input {
59+
background-color: var(--bs-gray-400);
60+
}
61+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { EventEmitter } from '@angular/core';
3+
import {
4+
ComponentFixture,
5+
inject,
6+
TestBed,
7+
} from '@angular/core/testing';
8+
import { ReactiveFormsModule } from '@angular/forms';
9+
import { By } from '@angular/platform-browser';
10+
import {
11+
DYNAMIC_FORM_CONTROL_MAP_FN,
12+
DynamicFormLayoutService,
13+
DynamicFormService,
14+
DynamicFormValidationService,
15+
DynamicInputModel,
16+
} from '@ng-dynamic-forms/core';
17+
import { provideMockStore } from '@ngrx/store/testing';
18+
import {
19+
TranslateModule,
20+
TranslateService,
21+
} from '@ngx-translate/core';
22+
import { NgxMaskModule } from 'ngx-mask';
23+
import { of } from 'rxjs';
24+
25+
import {
26+
APP_CONFIG,
27+
APP_DATA_SERVICES_MAP,
28+
} from '../../../../../../../config/app-config.interface';
29+
import { environment } from '../../../../../../../environments/environment.test';
30+
import { SubmissionService } from '../../../../../../submission/submission.service';
31+
import { DsDynamicFormControlContainerComponent } from '../../ds-dynamic-form-control-container.component';
32+
import { dsDynamicFormControlMapFn } from '../../ds-dynamic-form-control-map-fn';
33+
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
34+
import { DsDynamicFormArrayComponent } from './dynamic-form-array.component';
35+
36+
describe('DsDynamicFormArrayComponent', () => {
37+
const translateServiceStub = {
38+
get: () => of('translated-text'),
39+
instant: () => 'translated-text',
40+
onLangChange: new EventEmitter(),
41+
onTranslationChange: new EventEmitter(),
42+
onDefaultLangChange: new EventEmitter(),
43+
};
44+
45+
let component: DsDynamicFormArrayComponent;
46+
let fixture: ComponentFixture<DsDynamicFormArrayComponent>;
47+
48+
beforeEach(async () => {
49+
await TestBed.configureTestingModule({
50+
imports: [
51+
ReactiveFormsModule,
52+
DsDynamicFormArrayComponent,
53+
NgxMaskModule.forRoot(),
54+
TranslateModule.forRoot(),
55+
],
56+
providers: [
57+
DynamicFormLayoutService,
58+
DynamicFormValidationService,
59+
provideMockStore(),
60+
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
61+
{ provide: TranslateService, useValue: translateServiceStub },
62+
{ provide: HttpClient, useValue: {} },
63+
{ provide: SubmissionService, useValue: {} },
64+
{ provide: APP_CONFIG, useValue: environment },
65+
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
66+
],
67+
}).overrideComponent(DsDynamicFormArrayComponent, {
68+
remove: {
69+
imports: [DsDynamicFormControlContainerComponent],
70+
},
71+
})
72+
.compileComponents();
73+
});
74+
75+
beforeEach(inject([DynamicFormService], (service: DynamicFormService) => {
76+
const formModel = [
77+
new DynamicRowArrayModel({
78+
id: 'testFormRowArray',
79+
initialCount: 5,
80+
notRepeatable: false,
81+
relationshipConfig: undefined,
82+
submissionId: '1234',
83+
isDraggable: true,
84+
groupFactory: () => {
85+
return [
86+
new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }),
87+
];
88+
},
89+
required: false,
90+
metadataKey: 'dc.contributor.author',
91+
metadataFields: ['dc.contributor.author'],
92+
hasSelectableMetadata: true,
93+
showButtons: true,
94+
typeBindRelations: [{ match: 'VISIBLE', operator: 'OR', when: [{ id: 'dc.type', value: 'Book' }] }],
95+
}),
96+
];
97+
98+
fixture = TestBed.createComponent(DsDynamicFormArrayComponent);
99+
component = fixture.componentInstance;
100+
component.model = formModel[0] as DynamicRowArrayModel;
101+
102+
component.group = service.createFormGroup(formModel);
103+
104+
fixture.detectChanges();
105+
}));
106+
107+
it('should move element up and maintain focus', () => {
108+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
109+
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowUp' }), dropList, 3, 1, 'up');
110+
fixture.detectChanges();
111+
expect(component.model.groups[0]).toBeDefined();
112+
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[0]);
113+
});
114+
115+
it('should move element down and maintain focus', () => {
116+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
117+
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 1, 'down');
118+
fixture.detectChanges();
119+
expect(component.model.groups[2]).toBeDefined();
120+
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
121+
});
122+
123+
it('should wrap around when moving up from the first element', () => {
124+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
125+
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowUp' }), dropList, 3, 0, 'up');
126+
fixture.detectChanges();
127+
expect(component.model.groups[2]).toBeDefined();
128+
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
129+
});
130+
131+
it('should wrap around when moving down from the last element', () => {
132+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
133+
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 2, 'down');
134+
fixture.detectChanges();
135+
expect(component.model.groups[0]).toBeDefined();
136+
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[0]);
137+
});
138+
139+
it('should not move element if keyboard drag is not active', () => {
140+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
141+
component.elementBeingSorted = null;
142+
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 1, 'down');
143+
fixture.detectChanges();
144+
expect(component.model.groups[1]).toBeDefined();
145+
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
146+
});
147+
148+
it('should cancel keyboard drag and drop', () => {
149+
const dropList = fixture.debugElement.query(By.css('.cdk-drop-list')).nativeElement;
150+
component.elementBeingSortedStartingIndex = 2;
151+
component.elementBeingSorted = dropList.querySelectorAll('[cdkDragHandle]')[2];
152+
component.model.moveGroup(2, 1);
153+
fixture.detectChanges();
154+
component.cancelKeyboardDragAndDrop(dropList, 1, 3);
155+
fixture.detectChanges();
156+
expect(component.elementBeingSorted).toBeNull();
157+
expect(component.elementBeingSortedStartingIndex).toBeNull();
158+
});
159+
});

0 commit comments

Comments
 (0)