Skip to content
Open
5 changes: 5 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ submission:
- value: default
style: text-muted
icon: fa-circle-xmark
# If set to true avoid setting placeholder for simple fields, where the placeholder would be the same as the label.
# The default is set to true as placeholders that do not provide additional information to the field are to be avoided as they could cause accessibility issue.
# More info on the topic can be found at https://www.deque.com/blog/accessible-forms-the-problem-with-placeholders/
omitSimpleFieldPlaceholders:


# Default Language in which the UI will be rendered if the user's browser language is not an active language
defaultLanguage: en
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<div [class.mb-2]="hasLabel || model.type === 'DATE' || (model.type !== 'GROUP' && asBootstrapFormGroup) || getClass('element', 'container').includes('mb-2')"
[class.d-none]="model.hidden"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
@if (!isCheckbox && hasLabel) {
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
@if (!isCheckbox && hasLabel && !isDateField) {
<label
[id]="'label_' + model.id"
[for]="id"
class="form-label"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
}
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
Expand All @@ -20,8 +20,7 @@
</div>

@if (hasHint && (formBuilderService.hasArrayGroupValue(model) || (!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)) {
<small
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<small 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 -->
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
const testItem: Item = new Item();
const testWSI: WorkspaceItem = new WorkspaceItem();
testWSI.item = of(createSuccessfulRemoteDataObject(testItem));
const renderer = jasmine.createSpyObj('Renderer2', ['setAttribute']);

beforeEach(waitForAsync(() => {

TestBed.configureTestingModule({
Expand Down Expand Up @@ -268,6 +270,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
});

fixture.detectChanges();
renderer.setAttribute.calls.reset();
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
}));

Expand Down Expand Up @@ -380,4 +383,43 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
});

it('should not show a label if is a checkbox or a date field', () => {
const checkboxLabel = fixture.debugElement.query(By.css('#label_' + formModel[0].id));
const dsDatePickerLabel = fixture.debugElement.query(By.css('#label_' + formModel[22].id));

expect(checkboxLabel).toBeNull();
expect(dsDatePickerLabel).toBeNull();
});

it('should not call handleAriaLabelForLibraryComponents if is SSR', () => {
(component as any).platformId = 'server';
(component as any).componentRef = {
instance: new DynamicNGBootstrapInputComponent(null, null),
location: { nativeElement: document.createElement('div') },
} as any;
fixture.detectChanges();

(component as any).handleAriaLabelForLibraryComponents();

expect(renderer.setAttribute).not.toHaveBeenCalled();
});

it('should set aria-label when valid input and additional property ariaLabel exist and is on browser', () => {
(component as any).platformId = 'browser';
const inputEl = document.createElement('input');
const hostEl = {
querySelector: jasmine.createSpy('querySelector').and.returnValue(inputEl),
};

(component as any).componentRef = {
instance: new DynamicNGBootstrapInputComponent(null, null),
location: { nativeElement: hostEl },
} as any;
(component as any).renderer = renderer;
component.model = { additional: { ariaLabel: 'Accessible Label' } } as any;
fixture.detectChanges();
(component as any).handleAriaLabelForLibraryComponents();
expect(renderer.setAttribute).toHaveBeenCalledWith(inputEl, 'aria-label', 'Accessible Label');
});

});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AsyncPipe,
isPlatformBrowser,
NgClass,
NgTemplateOutlet,
} from '@angular/common';
Expand All @@ -18,7 +19,9 @@ import {
OnDestroy,
OnInit,
Output,
PLATFORM_ID,
QueryList,
Renderer2,
SimpleChanges,
Type,
ViewChild,
Expand Down Expand Up @@ -77,10 +80,12 @@ import {
import {
DYNAMIC_FORM_CONTROL_MAP_FN,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
DynamicFormArrayComponent,
DynamicFormArrayGroupModel,
DynamicFormArrayModel,
DynamicFormComponentService,
DynamicFormControl,
DynamicFormControlComponent,
DynamicFormControlContainerComponent,
DynamicFormControlEvent,
DynamicFormControlEventType,
Expand Down Expand Up @@ -120,6 +125,7 @@ import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relatio
import { ExistingMetadataListElementComponent } from './existing-metadata-list-element/existing-metadata-list-element.component';
import { ExistingRelationListElementComponent } from './existing-relation-list-element/existing-relation-list-element.component';
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
import { NameVariantService } from './relation-lookup-modal/name-variant.service';

Expand Down Expand Up @@ -211,6 +217,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
protected metadataService: MetadataService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
protected renderer: Renderer2,
@Inject(PLATFORM_ID) protected platformId: string,
) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
Expand Down Expand Up @@ -300,6 +308,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
}


get isDateField(): boolean {
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
}

ngOnChanges(changes: SimpleChanges) {
if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) {
super.ngOnChanges(changes);
Expand All @@ -321,6 +334,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo

ngAfterViewInit() {
this.showErrorMessagesPreviousStage = this.showErrorMessages;
this.handleAriaLabelForLibraryComponents();
}

protected createFormControlComponent(): void {
Expand Down Expand Up @@ -462,4 +476,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
this.subs.push(collection$.subscribe((collection) => this.collection = collection));

}

private handleAriaLabelForLibraryComponents(): void {
if (!isPlatformBrowser(this.platformId)) {
return;
}

if ((this.componentRef.instance instanceof DynamicFormControlComponent) &&
!(this.componentRef.instance instanceof DynamicFormArrayComponent) &&
this.componentRef.location.nativeElement) {
const inputEl: HTMLElement | null =
this.componentRef.location.nativeElement.querySelector('input,textarea,select,[role="textbox"]');


if (inputEl && this.model?.additional?.ariaLabel) {
this.renderer.setAttribute(inputEl, 'aria-label', this.model.additional.ariaLabel);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<fieldset class="d-flex">
@if (!model.repeatable) {
<legend [id]="'legend_' + model.id" [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
{{model.placeholder}} @if (model.required) {
{{model.label}} @if (model.required) {
<span>*</span>
}
</legend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
class="form-control"
[attr.aria-labelledby]="'label_' + model.id"
[attr.autoComplete]="model.autoComplete"
[attr.aria-label]="model.label | translate"
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate"
[class.is-invalid]="showErrorMessages"
[id]="model.id"
[inputFormatter]="formatter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
<input class="form-control"
[attr.aria-controls]="'combobox_' + id + '_listbox'"
[attr.aria-label]="model.placeholder"
[attr.aria-label]="model.label"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[class.scrollable-dropdown-input]="!model.readOnly"
Expand All @@ -32,11 +32,11 @@

<div #dropdownMenu ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
[attr.aria-label]="model.placeholder | translate">
[attr.aria-label]="model.label | translate">
<div class="scrollable-menu"
role="listbox"
[id]="'combobox_' + id + '_listbox'"
[attr.aria-label]="model.placeholder | translate"
[attr.aria-label]="model.label | translate"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
Expand Down
8 changes: 8 additions & 0 deletions src/app/shared/form/builder/parsers/date-field-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,12 @@ describe('DateFieldParser test suite', () => {

expect(fieldModel.value).toEqual(expectedValue);
});

it('should skip setting the placeholder when ignore omitSimpleFieldPlaceholders is true', () => {
const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService);
parser.omitSimpleFieldPlaceholders = true;
const fieldModel = parser.parse();

expect(fieldModel.placeholder).toBeNull();
});
});
4 changes: 2 additions & 2 deletions src/app/shared/form/builder/parsers/date-field-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export class DateFieldParser extends FieldParser {

public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any {
let malformedDate = false;
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true);
inputDateModelConfig.legend = this.configData.label;
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, label, true);
inputDateModelConfig.legend = this.configData.repeatable ? null : this.configData.label;
inputDateModelConfig.disabled = inputDateModelConfig.readOnly;
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
this.setValues(inputDateModelConfig as any, fieldValue);
Expand Down
10 changes: 9 additions & 1 deletion src/app/shared/form/builder/parsers/field-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { DynamicFormControlLayout } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import uniqueId from 'lodash/uniqueId';
import { environment } from 'src/environments/environment';

import {
DsDynamicInputModel,
Expand Down Expand Up @@ -53,13 +54,16 @@ export abstract class FieldParser {
*/
protected typeField: string;

omitSimpleFieldPlaceholders: boolean;

constructor(
@Inject(SUBMISSION_ID) protected submissionId: string,
@Inject(CONFIG_DATA) protected configData: FormFieldModel,
@Inject(INIT_FORM_VALUES) protected initFormValues: any,
@Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions,
protected translate: TranslateService,
) {
this.omitSimpleFieldPlaceholders = environment.submission.omitSimpleFieldPlaceholders;
}

public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
Expand Down Expand Up @@ -306,7 +310,11 @@ export abstract class FieldParser {
if (hint) {
controlModel.hint = this.configData.hints || '&nbsp;';
}
controlModel.placeholder = this.configData.label;
if (!this.omitSimpleFieldPlaceholders) {
controlModel.placeholder = this.configData.label;
} else {
controlModel.additional = { ...controlModel.additional, ariaLabel: this.configData.label };
}

if (this.configData.mandatory && setErrors) {
this.markAsRequired(controlModel);
Expand Down
1 change: 1 addition & 0 deletions src/config/default-app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export class DefaultAppConfig implements AppConfig {
],
},
},
omitSimpleFieldPlaceholders: true,
};

// Default Language in which the UI will be rendered if the user's browser language is not an active language
Expand Down
1 change: 1 addition & 0 deletions src/config/submission-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export interface SubmissionConfig extends Config {
duplicateDetection: DuplicateDetectionConfig;
typeBind: TypeBindConfig;
icons: IconsConfig;
omitSimpleFieldPlaceholders?: boolean;
}
1 change: 1 addition & 0 deletions src/environments/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export const environment: BuildConfig = {
],
},
},
omitSimpleFieldPlaceholders: false,
},

// NOTE: will log all redux actions and transfers in console
Expand Down