1
0

Merge pull request #1346 from 4Science/CST-4659

[Deque Re-Analysis] Submission "critical" accessibility issues
This commit is contained in:
Tim Donohue
2021-10-18 11:01:21 -05:00
committed by GitHub
17 changed files with 231 additions and 144 deletions

View File

@@ -1,79 +1,80 @@
<div [class.form-group]="(model.type !== 'GROUP' && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
<label *ngIf="!isCheckbox && hasLabel"
[id]="'label_' + model.id"
[for]="id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model"></ng-container>
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
<label *ngIf="!isCheckbox && hasLabel"
[id]="'label_' + model.id"
[for]="id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model"></ng-container>
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
<div [ngClass]="getClass('grid', 'control')">
<ng-container #componentViewContainer></ng-container>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null
<div [ngClass]="getClass('grid', 'control')">
<div>
<ng-container #componentViewContainer></ng-container>
</div>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null
&& (!showErrorMessages || errorMessages.length === 0)" class="clearfix w-100 mb-2"></div>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
</div>
<div *ngIf="showErrorMessages" [id]="id + '_errors'"
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
</div>
</div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
<div *ngIf="isRelationship" class="col-auto text-center">
<button class="btn btn-secondary"
type="button"
ngbTooltip="{{'form.lookup-help' | translate}}"
placement="top"
(click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
</button>
</div>
</div>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
<ng-container *ngIf="value?.isVirtual">
<ds-existing-metadata-list-element
*ngIf="model.hasSelectableMetadata"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
(remove)="onRemove()"
>
</ds-existing-metadata-list-element>
<ds-existing-relation-list-element
*ngIf="!model.hasSelectableMetadata"
[ngClass]="{'d-block pb-2 pt-2': !context?.index}"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
>
</ds-existing-relation-list-element>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div class="clearfix w-100 mb-2"></div>
</ng-container>
<ng-content></ng-content>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
<div *ngIf="isRelationship" class="col-auto text-center">
<button class="btn btn-secondary"
type="button"
ngbTooltip="{{'form.lookup-help' | translate}}"
placement="top"
(click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
</button>
</div>
</div>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
<ng-container *ngIf="value?.isVirtual">
<ds-existing-metadata-list-element
*ngIf="model.hasSelectableMetadata"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
(remove)="onRemove()"
>
</ds-existing-metadata-list-element>
<ds-existing-relation-list-element
*ngIf="!model.hasSelectableMetadata"
[ngClass]="{'d-block pb-2 pt-2': !context?.index}"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
>
</ds-existing-relation-list-element>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div class="clearfix w-100 mb-2"></div>
</ng-container>
<ng-content></ng-content>
</div>

View File

@@ -1,50 +1,54 @@
<div class="d-flex">
<ds-number-picker
tabindex="1"
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="showErrorMessages"
[placeholder]='yearPlaceholder'
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<div>
<fieldset class="d-flex">
<legend [id]="'legend_' + model.id" [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
{{model.placeholder}} <span *ngIf="model.required">*</span>
</legend>
<ds-number-picker
tabindex="1"
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="showErrorMessages"
[placeholder]='yearPlaceholder'
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="2"
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="3"
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="2"
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="3"
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
</fieldset>
</div>
<div class="clearfix"></div>

View File

@@ -1,3 +1,7 @@
.col-lg-1 {
width: auto;
}
legend {
font-size: initial;
}

View File

@@ -69,6 +69,7 @@ describe('DsDatePickerComponent test suite', () => {
[bindId]='bindId'
[group]='group'
[model]='model'
[legend]='legend'
(blur)='onBlur($event)'
(change)='onValueChange($event)'
(focus)='onFocus($event)'></ds-date-picker>`;

View File

@@ -20,6 +20,7 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicDsDatePickerModel;
@Input() legend: string;
@Output() selected = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();

View File

@@ -1,24 +1,30 @@
import {
DynamicDateControlModel,
DynamicDateControlModelConfig,
DynamicDatePickerModelConfig,
DynamicFormControlLayout,
serializable
} from '@ng-dynamic-forms/core';
export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE';
export interface DynamicDsDateControlModelConfig extends DynamicDatePickerModelConfig {
legend?: string;
}
/**
* Dynamic Date Picker Model class
*/
export class DynamicDsDatePickerModel extends DynamicDateControlModel {
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
malformedDate: boolean;
legend: string;
hasLanguages = false;
repeatable = false;
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
constructor(config: DynamicDsDateControlModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.malformedDate = false;
this.legend = config.legend;
}
}

View File

@@ -1,8 +1,13 @@
<div #sdRef="ngbDropdown" ngbDropdown display="dynamic" placement="bottom-right" class="w-100">
<div class="position-relative right-addon">
<div class="position-relative right-addon"
role="combobox"
[attr.aria-label]="model.label"
[attr.aria-owns]="'combobox_' + id + '_listbox'">
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
aria-hidden="true"></i>
<input class="form-control"
[attr.aria-controls]="'combobox_' + id + '_listbox'"
[attr.aria-activedescendant]="'combobox_' + id + '_selected'"
[attr.aria-label]="model.placeholder"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
@@ -24,6 +29,8 @@
aria-expanded="false"
[attr.aria-label]="model.placeholder">
<div class="scrollable-menu"
role="listbox"
[id]="'combobox_' + id + '_listbox'"
[attr.aria-label]="model.placeholder"
infiniteScroll
[infiniteScrollDistance]="2"
@@ -32,7 +39,10 @@
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry); sdRef.close()" title="{{ listEntry.display }}">
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
(click)="onSelect(listEntry); sdRef.close()"
title="{{ listEntry.display }}" role="option"
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
@@ -40,5 +50,3 @@
</div>
</div>

View File

@@ -1,6 +1,8 @@
import { FieldParser } from './field-parser';
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import {
DynamicDsDateControlModelConfig,
DynamicDsDatePickerModel
} from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { isNotEmpty } from '../../../empty.util';
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
@@ -9,7 +11,8 @@ export class DateFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let malformedDate = false;
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true);
inputDateModelConfig.legend = this.configData.label;
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
this.setValues(inputDateModelConfig as any, fieldValue);

View File

@@ -1,6 +1,6 @@
<div class="d-flex flex-column align-items-center justify-content-around mr-3">
<button
class="btn btn-link"
class="btn btn-link-focus"
type="button"
tabindex="0"
[disabled]="disabled"
@@ -25,7 +25,7 @@
aria-label="name"
>
<button
class="btn btn-link"
class="btn btn-link-focus"
type="button"
tabindex="0"
[disabled]="disabled"

View File

@@ -23,3 +23,24 @@
input {
max-width: 80px !important;
}
.btn-link-focus {
// behave as btn-link but does not override box-shadow of btn-link:focus
font-weight: $font-weight-normal;
color: $link-color;
text-decoration: $link-decoration;
@include hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
pointer-events: none;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
}
}

View File

@@ -1,5 +1,6 @@
export const mockDynamicFormLayoutService = jasmine.createSpyObj('DynamicFormLayoutService', {
getElementId: jasmine.createSpy('getElementId')
getElementId: jasmine.createSpy('getElementId'),
getClass: 'class',
});
export const mockDynamicFormValidationService = jasmine.createSpyObj('DynamicFormValidationService', {

View File

@@ -19,11 +19,14 @@
(fileOver)="fileOverBase($event)"
class="well ds-base-drop-zone mt-1 mb-3 text-muted">
<div class="text-center m-0 p-2 d-flex justify-content-center align-items-center" *ngIf="uploader?.queue?.length === 0">
<span><i class="fas fa-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}}</span>
<label for="inputFileUploader" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
<input #fileInput id="inputFileUploader" class="d-none" type="file" role="button" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
{{'uploader.browse' | translate}}
</label>
<span>
<i class="fas fa-upload" aria-hidden="true"></i>
{{dropMsg | translate}}{{'uploader.or' | translate}}
<label for="inputFileUploader" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
<span role="button" [attr.aria-label]="'uploader.browse' | translate">{{'uploader.browse' | translate}}</span>
</label>
<input #fileInput id="inputFileUploader" class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
</span>
</div>
<div *ngIf="(isOverBaseDropZone | async) || uploader?.queue?.length !== 0">
<div class="m-1">

View File

@@ -17,3 +17,23 @@
z-index: var(--ds-submission-footer-z-index);
}
.btn-link-focus {
// behave as btn-link but does not override box-shadow of btn-link:focus
font-weight: $font-weight-normal;
color: $link-color;
text-decoration: $link-decoration;
@include hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
pointer-events: none;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
}
}

View File

@@ -15,15 +15,15 @@
<span class="float-left section-title" tabindex="0">{{ 'submission.sections.'+sectionData.header | translate }}</span>
<div class="d-inline-block float-right">
<i *ngIf="!(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-warning mr-3"
aria-hidden="true" title="{{'submission.sections.status.warnings.title' | translate}}"></i>
title="{{'submission.sections.status.warnings.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.warnings.aria' | translate"></i>
<i *ngIf="(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-danger mr-3"
aria-hidden="true" title="{{'submission.sections.status.errors.title' | translate}}"></i>
title="{{'submission.sections.status.errors.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.errors.aria' | translate"></i>
<i *ngIf="(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-check-circle text-success mr-3"
aria-hidden="true" title="{{'submission.sections.status.valid.title' | translate}}"></i>
title="{{'submission.sections.status.valid.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.valid.aria' | translate"></i>
<a class="close"
tabindex="0"
role="button"
[attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate"
[attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.aria.close' : 'submission.sections.toggle.aria.open') | translate: {sectionHeader: ('submission.sections.'+sectionData.header | translate)}"
[title]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate">
<span *ngIf="sectionRef.isOpen()" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!sectionRef.isOpen()" class="fas fa-chevron-down fa-fw"></span>

View File

@@ -74,7 +74,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
required: 'submission.sections.upload.form.date-required-from'
}
};
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
@@ -104,7 +104,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
required: 'submission.sections.upload.form.date-required-until'
}
};
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {

View File

@@ -10,16 +10,16 @@
</div>
<div class="float-right w-15" [class.sticky-buttons]="!readMode">
<ng-container *ngIf="readMode">
<ds-file-download-link [cssClasses]="'btn btn-link'" [isBlank]="true" [bitstream]="getBitstream()">
<ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()">
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
</ds-file-download-link>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.edit.title' | translate"
title="{{ 'submission.sections.upload.edit.title' | translate }}"
(click)="$event.preventDefault();switchMode();">
<i class="fa fa-edit fa-2x text-normal"></i>
</button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.delete.confirm.title' | translate"
title="{{ 'submission.sections.upload.delete.confirm.title' | translate }}"
[disabled]="(processingDelete$ | async)"
@@ -29,17 +29,17 @@
</button>
</ng-container>
<ng-container *ngIf="!readMode">
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.save-metadata' | translate"
title="{{ 'submission.sections.upload.save-metadata' | translate }}"
(click)="saveBitstreamData($event);">
<i class="fa fa-save fa-2x text-success"></i>
</button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.undo' | translate"
title="{{ 'submission.sections.upload.undo' | translate }}"
(click)="$event.preventDefault();switchMode();"><i class="fa fa-ban fa-2x text-warning"></i></button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.delete.confirm.title' | translate"
title="{{ 'submission.sections.upload.delete.confirm.title' | translate }}"
[disabled]="(processingDelete$ | async)"

View File

@@ -3606,10 +3606,20 @@
"submission.sections.status.warnings.title": "Warnings",
"submission.sections.status.errors.aria": "has errors",
"submission.sections.status.valid.aria": "is valid",
"submission.sections.status.warnings.aria": "has warnings",
"submission.sections.toggle.open": "Open section",
"submission.sections.toggle.close": "Close section",
"submission.sections.toggle.aria.open": "Expand {{sectionHeader}} section",
"submission.sections.toggle.aria.close": "Collapse {{sectionHeader}} section",
"submission.sections.upload.delete.confirm.cancel": "Cancel",
"submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?",
@@ -3630,6 +3640,10 @@
"submission.sections.upload.form.date-required": "Date is required.",
"submission.sections.upload.form.date-required-from": "Grant access from date is required.",
"submission.sections.upload.form.date-required-until": "Grant access until date is required.",
"submission.sections.upload.form.from-label": "Grant access from",
"submission.sections.upload.form.from-placeholder": "From",