Merge pull request #3269 from atmire/w2p-117544_support-for-disabled-elements-for-screen-readers-8.0

[Port dspace-8_x] support for disabled elements for screen readers
This commit is contained in:
Tim Donohue
2025-01-28 16:19:21 -06:00
committed by GitHub
188 changed files with 866 additions and 237 deletions

View File

@@ -293,7 +293,8 @@
], ],
"rules": { "rules": {
// Custom DSpace Angular rules // Custom DSpace Angular rules
"dspace-angular-html/themed-component-usages": "error" "dspace-angular-html/themed-component-usages": "error",
"dspace-angular-html/no-disabled-attribute-on-button": "error"
} }
}, },
{ {

View File

@@ -2,3 +2,4 @@
_______ _______
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class - [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.

View File

@@ -0,0 +1,78 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
_______
Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
_______
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
### Examples
#### Valid code
##### should use [dsBtnDisabled] in HTML templates
```html
<button [dsBtnDisabled]="true">Submit</button>
```
##### disabled attribute is still valid on non-button elements
```html
<input disabled>
```
##### [disabled] attribute is still valid on non-button elements
```html
<input [disabled]="true">
```
##### angular dynamic attributes that use disabled are still valid
```html
<button [class.disabled]="isDisabled">Submit</button>
```
#### Invalid code &amp; automatic fixes
##### should not use disabled attribute in HTML templates
```html
<button disabled>Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```
##### should not use [disabled] attribute in HTML templates
```html
<button [disabled]="true">Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```

View File

@@ -10,10 +10,13 @@ import {
bundle, bundle,
RuleExports, RuleExports,
} from '../../util/structure'; } from '../../util/structure';
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages'; import * as themedComponentUsages from './themed-component-usages';
const index = [ const index = [
themedComponentUsages, themedComponentUsages,
noDisabledAttributeOnButton,
] as unknown as RuleExports[]; ] as unknown as RuleExports[];
export = { export = {

View File

@@ -0,0 +1,147 @@
import {
TmplAstBoundAttribute,
TmplAstTextAttribute,
} from '@angular-eslint/bundled-angular-compiler';
import { TemplateParserServices } from '@angular-eslint/utils';
import {
ESLintUtils,
TSESLint,
} from '@typescript-eslint/utils';
import {
DSpaceESLintRuleInfo,
NamedTests,
} from '../../util/structure';
import { getSourceCode } from '../../util/typescript';
export enum Message {
USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
}
export const info = {
name: 'no-disabled-attribute-on-button',
meta: {
docs: {
description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
},
type: 'problem',
fixable: 'code',
schema: [],
messages: {
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
},
},
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: TSESLint.RuleContext<Message, unknown[]>) {
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
/**
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
*/
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
}
/**
* Replace the disabled text with [dsBtnDisabled] in the template
*/
function replaceDisabledText(text: string ): string {
const hasBrackets = text.includes('[') && text.includes(']');
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
return text.replace('disabled', newDisabledText);
}
function inputIsChildOfButton(node: any): boolean {
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
}
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
return;
}
const sourceSpan = node.sourceSpan;
context.report({
messageId: Message.USE_DSBTN_DISABLED,
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
fix(fixer) {
const templateText = sourceSpan.start.file.content;
const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
const newText = replaceDisabledText(disabledText);
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
},
});
}
return {
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
reportAndFix(node);
},
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
reportAndFix(node);
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'should use [dsBtnDisabled] in HTML templates',
code: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'disabled attribute is still valid on non-button elements',
code: `
<input disabled>
`,
},
{
name: '[disabled] attribute is still valid on non-button elements',
code: `
<input [disabled]="true">
`,
},
{
name: 'angular dynamic attributes that use disabled are still valid',
code: `
<button [class.disabled]="isDisabled">Submit</button>
`,
},
],
invalid: [
{
name: 'should not use disabled attribute in HTML templates',
code: `
<button disabled>Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'should not use [disabled] attribute in HTML templates',
code: `
<button [disabled]="true">Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
],
} as NamedTests;
export default rule;

View File

@@ -10,7 +10,7 @@
<button class="btn btn-outline-primary mr-3" (click)="reset()"> <button class="btn btn-outline-primary mr-3" (click)="reset()">
{{ 'access-control-cancel' | translate }} {{ 'access-control-cancel' | translate }}
</button> </button>
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()"> <button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
{{ 'access-control-execute' | translate }} {{ 'access-control-execute' | translate }}
</button> </button>
</div> </div>

View File

@@ -14,6 +14,7 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
@@ -27,6 +28,7 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
TranslateModule, TranslateModule,
BulkAccessSettingsComponent, BulkAccessSettingsComponent,
BulkAccessBrowseComponent, BulkAccessBrowseComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -42,6 +42,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
@@ -151,7 +152,7 @@ describe('EPeopleRegistryComponent', () => {
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]), imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
TranslateModule.forRoot(), EPeopleRegistryComponent], TranslateModule.forRoot(), EPeopleRegistryComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },

View File

@@ -25,7 +25,7 @@
</button> </button>
</div> </div>
<div *ngIf="displayResetPassword" between class="btn-group"> <div *ngIf="displayResetPassword" between class="btn-group">
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()"> <button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}} <i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button> </button>
</div> </div>

View File

@@ -43,6 +43,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
@@ -221,7 +222,7 @@ describe('EPersonFormComponent', () => {
route = new ActivatedRouteStub(); route = new ActivatedRouteStub();
router = new RouterStub(); router = new RouterStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BtnDisabledDirective, BrowserModule,
RouterModule.forRoot([]), RouterModule.forRoot([]),
TranslateModule.forRoot(), TranslateModule.forRoot(),
EPersonFormComponent, EPersonFormComponent,
@@ -516,7 +517,8 @@ describe('EPersonFormComponent', () => {
// ePersonDataServiceStub.activeEPerson = eperson; // ePersonDataServiceStub.activeEPerson = eperson;
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204)); spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
const deleteButton = fixture.debugElement.query(By.css('.delete-button')); const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
expect(deleteButton.nativeElement.disabled).toBe(false); expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
deleteButton.triggerEventHandler('click', null); deleteButton.triggerEventHandler('click', null);
fixture.detectChanges(); fixture.detectChanges();
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson); expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);

View File

@@ -65,6 +65,7 @@ import {
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
@@ -92,6 +93,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
PaginationComponent, PaginationComponent,
RouterLink, RouterLink,
HasNoValuePipe, HasNoValuePipe,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -35,14 +35,14 @@
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)" <button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
*ngIf="epersonDTO.ableToDelete" *ngIf="epersonDTO.ableToDelete"
[disabled]="actionConfig.remove.disabled" [dsBtnDisabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]" [ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i> <i [ngClass]="actionConfig.remove.icon"></i>
</button> </button>
<button *ngIf="!epersonDTO.ableToDelete" <button *ngIf="!epersonDTO.ableToDelete"
(click)="addMemberToGroup(epersonDTO.eperson)" (click)="addMemberToGroup(epersonDTO.eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
@@ -122,7 +122,7 @@
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="addMemberToGroup(eperson)" <button (click)="addMemberToGroup(eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>

View File

@@ -54,6 +54,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
@@ -113,6 +114,7 @@ export interface EPersonListActionConfig {
RouterLink, RouterLink,
NgClass, NgClass,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -69,7 +69,7 @@
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button *ngSwitchCase="false" <button *ngSwitchCase="false"
[disabled]="true" [dsBtnDisabled]="true"
class="btn btn-outline-primary btn-sm btn-edit" class="btn btn-outline-primary btn-sm btn-edit"
placement="left" placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate" [ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"

View File

@@ -50,6 +50,7 @@ import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
DSONameServiceMock, DSONameServiceMock,
UNDEFINED_NAME, UNDEFINED_NAME,
@@ -208,6 +209,7 @@ describe('GroupsRegistryComponent', () => {
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
GroupsRegistryComponent, GroupsRegistryComponent,
BtnDisabledDirective,
], ],
providers: [GroupsRegistryComponent, providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
@@ -278,7 +280,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });
@@ -312,7 +315,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });
}); });
@@ -331,7 +335,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeTrue(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
}); });
}); });
}); });

View File

@@ -62,6 +62,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -84,6 +85,7 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
NgSwitchCase, NgSwitchCase,
NgbTooltipModule, NgbTooltipModule,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -54,7 +54,7 @@
<div class="col-auto"> <div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button> <button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp; &nbsp;
<button class="btn btn-light" [disabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button> <button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button>
</div> </div>
</div> </div>
</div> </div>
@@ -158,8 +158,8 @@
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }} {{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
</div> </div>
<div> <div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [disabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button> <button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [disabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button> <button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<!-- <!--
<button id="export">{{'admin.reports.commons.export' | translate}}</button> <button id="export">{{'admin.reports.commons.export' | translate}}</button>
--> -->

View File

@@ -43,6 +43,7 @@ import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operator
import { isEmpty } from 'src/app/shared/empty.util'; import { isEmpty } from 'src/app/shared/empty.util';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FiltersComponent } from '../filters-section/filters-section.component'; import { FiltersComponent } from '../filters-section/filters-section.component';
import { FilteredItems } from './filtered-items-model'; import { FilteredItems } from './filtered-items-model';
import { OptionVO } from './option-vo.model'; import { OptionVO } from './option-vo.model';
@@ -64,6 +65,7 @@ import { QueryPredicate } from './query-predicate.model';
NgIf, NgIf,
NgForOf, NgForOf,
FiltersComponent, FiltersComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -6,10 +6,10 @@
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p> <p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right space-children-mr"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}} <i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
<span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span> <span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -15,6 +15,7 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
NgIf, NgIf,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -19,32 +19,32 @@
</div> </div>
<button *ngIf="(testConfigRunning$ |async) !== true" class="btn btn-secondary" <button *ngIf="(testConfigRunning$ |async) !== true" class="btn btn-secondary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="testConfiguration(contentSource)"> (click)="testConfiguration(contentSource)">
<span>{{'collection.source.controls.test.submit' | translate}}</span> <span>{{'collection.source.controls.test.submit' | translate}}</span>
</button> </button>
<button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary" <button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span>{{'collection.source.controls.test.running' | translate}}</span> <span>{{'collection.source.controls.test.running' | translate}}</span>
</button> </button>
<button *ngIf="(importRunning$ |async) !== true" class="btn btn-primary" <button *ngIf="(importRunning$ |async) !== true" class="btn btn-primary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="importNow()"> (click)="importNow()">
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span> <span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
</button> </button>
<button *ngIf="(importRunning$ |async)" class="btn btn-primary" <button *ngIf="(importRunning$ |async)" class="btn btn-primary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span> <span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
</button> </button>
<button *ngIf="(reImportRunning$ |async) !== true" class="btn btn-primary" <button *ngIf="(reImportRunning$ |async) !== true" class="btn btn-primary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="resetAndReimport()"> (click)="resetAndReimport()">
<span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.submit' | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.submit' | translate}}</span>
</button> </button>
<button *ngIf="(reImportRunning$ |async)" class="btn btn-primary" <button *ngIf="(reImportRunning$ |async)" class="btn btn-primary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.running' | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.running' | translate}}</span>
</button> </button>

View File

@@ -22,6 +22,7 @@ import { Collection } from '../../../../core/shared/collection.model';
import { ContentSource } from '../../../../core/shared/content-source.model'; import { ContentSource } from '../../../../core/shared/content-source.model';
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer'; import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
import { Process } from '../../../../process-page/processes/process.model'; import { Process } from '../../../../process-page/processes/process.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
@@ -104,7 +105,7 @@ describe('CollectionSourceControlsComponent', () => {
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']); requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective, BtnDisabledDirective],
providers: [ providers: [
{ provide: ScriptDataService, useValue: scriptDataService }, { provide: ScriptDataService, useValue: scriptDataService },
{ provide: ProcessDataService, useValue: processDataService }, { provide: ProcessDataService, useValue: processDataService },
@@ -193,9 +194,10 @@ describe('CollectionSourceControlsComponent', () => {
const buttons = fixture.debugElement.queryAll(By.css('button')); const buttons = fixture.debugElement.queryAll(By.css('button'));
expect(buttons[0].nativeElement.disabled).toBeTrue(); buttons.forEach(button => {
expect(buttons[1].nativeElement.disabled).toBeTrue(); expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(buttons[2].nativeElement.disabled).toBeTrue(); expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
});
}); });
it('should be enabled when isEnabled is true', () => { it('should be enabled when isEnabled is true', () => {
comp.shouldShow = true; comp.shouldShow = true;
@@ -205,9 +207,10 @@ describe('CollectionSourceControlsComponent', () => {
const buttons = fixture.debugElement.queryAll(By.css('button')); const buttons = fixture.debugElement.queryAll(By.css('button'));
expect(buttons[0].nativeElement.disabled).toBeFalse(); buttons.forEach(button => {
expect(buttons[1].nativeElement.disabled).toBeFalse(); expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(buttons[2].nativeElement.disabled).toBeFalse(); expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
});
}); });
it('should call the corresponding button when clicked', () => { it('should call the corresponding button when clicked', () => {
spyOn(comp, 'testConfiguration'); spyOn(comp, 'testConfiguration');

View File

@@ -40,6 +40,7 @@ import {
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { Process } from '../../../../process-page/processes/process.model'; import { Process } from '../../../../process-page/processes/process.model';
import { ProcessStatus } from '../../../../process-page/processes/process-status.model'; import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { VarDirective } from '../../../../shared/utils/var.directive'; import { VarDirective } from '../../../../shared/utils/var.directive';
@@ -56,6 +57,7 @@ import { VarDirective } from '../../../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
NgIf, NgIf,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,7 +1,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="d-inline-block float-right space-children-mr"> <div class="d-inline-block float-right space-children-mr">
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -12,7 +12,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)" [dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i (click)="onSubmit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
@@ -45,7 +45,7 @@
<div class="col-12"> <div class="col-12">
<div class="d-inline-block float-right ml-1 space-children-mr"> <div class="d-inline-block float-right ml-1 space-children-mr">
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -56,7 +56,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)" [dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i (click)="onSubmit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>

View File

@@ -56,6 +56,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -81,6 +82,7 @@ import { CollectionSourceControlsComponent } from './collection-source-controls/
ThemedLoadingComponent, ThemedLoadingComponent,
FormComponent, FormComponent,
CollectionSourceControlsComponent, CollectionSourceControlsComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -6,10 +6,10 @@
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p> <p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right space-children-mr"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
<i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}} <i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span>
<span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span> <span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -15,6 +15,7 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
VarDirective, VarDirective,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -77,29 +77,29 @@
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing" <button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing"
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
[disabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()"> [dsBtnDisabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing" <button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing"
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
[disabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)"> [dsBtnDisabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
<i class="fas fa-check fa-fw"></i> <i class="fas fa-check fa-fw"></i>
</button> </button>
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn" <button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
[disabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()"> [dsBtnDisabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn" <button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
[disabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()"> [dsBtnDisabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
<i class="fas fa-undo-alt fa-fw"></i> <i class="fas fa-undo-alt fa-fw"></i>
</button> </button>
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled" <button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [disabled]="disabled" cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}"> ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
<i class="fas fa-grip-vertical fa-fw"></i> <i class="fas fa-grip-vertical fa-fw"></i>

View File

@@ -34,6 +34,7 @@ import {
VIRTUAL_METADATA_PREFIX, VIRTUAL_METADATA_PREFIX,
} from '../../../core/shared/metadata.models'; } from '../../../core/shared/metadata.models';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component'; import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
@@ -188,6 +189,7 @@ describe('DsoEditMetadataValueComponent', () => {
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
DsoEditMetadataValueComponent, DsoEditMetadataValueComponent,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
providers: [ providers: [
{ provide: RelationshipDataService, useValue: relationshipService }, { provide: RelationshipDataService, useValue: relationshipService },
@@ -524,7 +526,14 @@ describe('DsoEditMetadataValueComponent', () => {
}); });
it(`should${disabled ? ' ' : ' not '}be disabled`, () => { it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
expect(btn.nativeElement.disabled).toBe(disabled); if (disabled) {
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
} else {
// Can be null or false, depending on if button was ever disabled so just check not true
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}
}); });
} else { } else {
it('should not exist', () => { it('should not exist', () => {

View File

@@ -67,6 +67,7 @@ import {
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model'; import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { isNotEmpty } from '../../../shared/empty.util'; import { isNotEmpty } from '../../../shared/empty.util';
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { import {
@@ -94,7 +95,7 @@ import {
styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'], styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'],
templateUrl: './dso-edit-metadata-value.component.html', templateUrl: './dso-edit-metadata-value.component.html',
standalone: true, standalone: true,
imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective], imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective, BtnDisabledDirective],
}) })
/** /**
* Component displaying a single editable row for a metadata value * Component displaying a single editable row for a metadata value

View File

@@ -1,18 +1,18 @@
<div class="item-metadata" *ngIf="form"> <div class="item-metadata" *ngIf="form">
<div class="button-row top d-flex my-2 space-children-mr ml-gap"> <div class="button-row top d-flex my-2 space-children-mr ml-gap">
<button class="mr-auto btn btn-success" id="dso-add-btn" [disabled]="form.newValue || (saving$ | async)" <button class="mr-auto btn btn-success" id="dso-add-btn" [dsBtnDisabled]="form.newValue || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate"
[title]="dsoType + '.edit.metadata.add-button' | translate" [title]="dsoType + '.edit.metadata.add-button' | translate"
(click)="add()"><i class="fas fa-plus" aria-hidden="true"></i> (click)="add()"><i class="fas fa-plus" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.add-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.add-button' | translate }}</span>
</button> </button>
<button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [disabled]="(saving$ | async)" <button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate" [title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i> (click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span>
</button> </button>
<button class="btn btn-primary ml-1" id="dso-save-btn" [disabled]="!hasChanges || (saving$ | async)" <button class="btn btn-primary ml-1" id="dso-save-btn" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
[title]="dsoType + '.edit.metadata.save-button' | translate" [title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"><i class="fas fa-save" aria-hidden="true"></i> (click)="submit()"><i class="fas fa-save" aria-hidden="true"></i>
@@ -21,7 +21,7 @@
<button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable" <button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable"
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
[title]="dsoType + '.edit.metadata.discard-button' | translate" [title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
(click)="discard()"><i class="fas fa-times" aria-hidden="true"></i> (click)="discard()"><i class="fas fa-times" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.discard-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
</button> </button>
@@ -77,13 +77,13 @@
</div> </div>
<div class="button-row bottom d-inline-block w-100"> <div class="button-row bottom d-inline-block w-100">
<div class="mt-2 float-right space-children-mr ml-gap"> <div class="mt-2 float-right space-children-mr ml-gap">
<button class="btn btn-warning" *ngIf="isReinstatable" [disabled]="(saving$ | async)" <button class="btn btn-warning" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate" [title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"> (click)="reinstate()">
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }} <i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
</button> </button>
<button class="btn btn-primary" [disabled]="!hasChanges || (saving$ | async)" <button class="btn btn-primary" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
[title]="dsoType + '.edit.metadata.save-button' | translate" [title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"> (click)="submit()">
@@ -92,7 +92,7 @@
<button class="btn btn-danger" *ngIf="!isReinstatable" <button class="btn btn-danger" *ngIf="!isReinstatable"
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
[title]="dsoType + '.edit.metadata.discard-button' | translate" [title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
(click)="discard()"> (click)="discard()">
<i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }} <i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
</button> </button>

View File

@@ -22,6 +22,7 @@ import { Item } from '../../core/shared/item.model';
import { ITEM } from '../../core/shared/item.resource-type'; import { ITEM } from '../../core/shared/item.resource-type';
import { MetadataValue } from '../../core/shared/metadata.models'; import { MetadataValue } from '../../core/shared/metadata.models';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TestDataService } from '../../shared/testing/test-data-service.mock'; import { TestDataService } from '../../shared/testing/test-data-service.mock';
@@ -94,6 +95,7 @@ describe('DsoEditMetadataComponent', () => {
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
DsoEditMetadataComponent, DsoEditMetadataComponent,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
providers: [ providers: [
{ provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap }, { provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap },
@@ -216,7 +218,13 @@ describe('DsoEditMetadataComponent', () => {
}); });
it(`should${disabled ? ' ' : ' not '}be disabled`, () => { it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
expect(btn.nativeElement.disabled).toBe(disabled); if (disabled) {
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
} else {
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}
}); });
} else { } else {
it('should not exist', () => { it('should not exist', () => {

View File

@@ -47,6 +47,7 @@ import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ResourceType } from '../../core/shared/resource-type'; import { ResourceType } from '../../core/shared/resource-type';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -66,7 +67,7 @@ import { MetadataFieldSelectorComponent } from './metadata-field-selector/metada
styleUrls: ['./dso-edit-metadata.component.scss'], styleUrls: ['./dso-edit-metadata.component.scss'],
templateUrl: './dso-edit-metadata.component.html', templateUrl: './dso-edit-metadata.component.html',
standalone: true, standalone: true,
imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule], imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component showing a table of all metadata on a DSpaceObject and options to modify them * Component showing a table of all metadata on a DSpaceObject and options to modify them

View File

@@ -28,7 +28,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<button <button
[disabled]="isInValid" [dsBtnDisabled]="isInValid"
class="btn btn-default btn-primary" class="btn btn-default btn-primary"
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button> (click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
</div> </div>

View File

@@ -29,6 +29,7 @@ import {
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component'; import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe'; import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
@@ -42,6 +43,7 @@ import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
ProfilePageSecurityFormComponent, ProfilePageSecurityFormComponent,
AsyncPipe, AsyncPipe,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -7,7 +7,7 @@
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button> <button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
<button id="button-save" type="submit" class="btn btn-primary" [disabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button> <button id="button-save" type="submit" class="btn btn-primary" [dsBtnDisabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
import { LogOutAction } from '../../core/auth/auth.actions'; import { LogOutAction } from '../../core/auth/auth.actions';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { EndUserAgreementComponent } from './end-user-agreement.component'; import { EndUserAgreementComponent } from './end-user-agreement.component';
@@ -57,7 +58,7 @@ describe('EndUserAgreementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), EndUserAgreementComponent], imports: [TranslateModule.forRoot(), EndUserAgreementComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: EndUserAgreementService, useValue: endUserAgreementService }, { provide: EndUserAgreementService, useValue: endUserAgreementService },
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
@@ -95,7 +96,8 @@ describe('EndUserAgreementComponent', () => {
it('should disable the save button', () => { it('should disable the save button', () => {
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement; const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
expect(button.disabled).toBeTruthy(); expect(button.getAttribute('aria-disabled')).toBe('true');
expect(button.classList.contains('disabled')).toBeTrue();
}); });
}); });

View File

@@ -23,6 +23,7 @@ import { AppState } from '../../app.reducer';
import { LogOutAction } from '../../core/auth/auth.actions'; import { LogOutAction } from '../../core/auth/auth.actions';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EndUserAgreementContentComponent } from './end-user-agreement-content/end-user-agreement-content.component'; import { EndUserAgreementContentComponent } from './end-user-agreement-content/end-user-agreement-content.component';
@@ -32,7 +33,7 @@ import { EndUserAgreementContentComponent } from './end-user-agreement-content/e
templateUrl: './end-user-agreement.component.html', templateUrl: './end-user-agreement.component.html',
styleUrls: ['./end-user-agreement.component.scss'], styleUrls: ['./end-user-agreement.component.scss'],
standalone: true, standalone: true,
imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule], imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component displaying the End User Agreement and an option to accept it * Component displaying the End User Agreement and an option to accept it

View File

@@ -41,7 +41,7 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="control-group col-sm-12 text-right"> <div class="control-group col-sm-12 text-right">
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button> <button [dsBtnDisabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -18,6 +18,7 @@ import { FeedbackDataService } from '../../../core/feedback/feedback-data.servic
import { Feedback } from '../../../core/feedback/models/feedback.model'; import { Feedback } from '../../../core/feedback/models/feedback.model';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../../core/services/route.service';
import { NativeWindowService } from '../../../core/services/window.service'; import { NativeWindowService } from '../../../core/services/window.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -45,7 +46,7 @@ describe('FeedbackFormComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), FeedbackFormComponent], imports: [TranslateModule.forRoot(), FeedbackFormComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() }, { provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
@@ -79,7 +80,8 @@ describe('FeedbackFormComponent', () => {
}); });
it('should have disabled button', () => { it('should have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue(); expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeTrue();
}); });
describe('when message is inserted', () => { describe('when message is inserted', () => {
@@ -90,7 +92,8 @@ describe('FeedbackFormComponent', () => {
}); });
it('should not have disabled button', () => { it('should not have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse(); expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeFalse();
}); });
it('on submit should call createFeedback of feedbackDataServiceStub service', () => { it('on submit should call createFeedback of feedbackDataServiceStub service', () => {

View File

@@ -30,6 +30,7 @@ import {
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ErrorComponent } from '../../../shared/error/error.component'; import { ErrorComponent } from '../../../shared/error/error.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -38,7 +39,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
templateUrl: './feedback-form.component.html', templateUrl: './feedback-form.component.html',
styleUrls: ['./feedback-form.component.scss'], styleUrls: ['./feedback-form.component.scss'],
standalone: true, standalone: true,
imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule], imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component displaying the contents of the Feedback Statement * Component displaying the contents of the Feedback Statement

View File

@@ -79,7 +79,7 @@
</a> </a>
<button <button
[disabled]="requestCopyForm.invalid" [dsBtnDisabled]="requestCopyForm.invalid"
class="btn btn-default btn-primary" class="btn btn-default btn-primary"
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button> (click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
</div> </div>

View File

@@ -55,6 +55,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -71,6 +72,7 @@ import { getItemPageRoute } from '../../item-page-routing-paths';
AsyncPipe, AsyncPipe,
ReactiveFormsModule, ReactiveFormsModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -16,7 +16,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting" <button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate" [attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
@@ -24,7 +24,7 @@
</button> </button>
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate" [attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
[disabled]="(hasChanges$ | async) !== true || submitting" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -57,7 +57,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting" <button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate" [attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
@@ -65,7 +65,7 @@
</button> </button>
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate" [attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
[disabled]="(hasChanges$ | async) !== true || submitting" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>

View File

@@ -51,6 +51,7 @@ import {
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -81,6 +82,7 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
NgForOf, NgForOf,
ThemedLoadingComponent, ThemedLoadingComponent,
AlertComponent, AlertComponent,
BtnDisabledDirective,
], ],
providers: [ObjectValuesPipe], providers: [ObjectValuesPipe],
standalone: true, standalone: true,

View File

@@ -113,13 +113,13 @@
title="{{'item.edit.bitstreams.edit.buttons.edit' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button [disabled]="!canRemove(update)" (click)="remove(entry.bitstream)" <button [dsBtnDisabled]="!canRemove(update)" (click)="remove(entry.bitstream)"
[attr.aria-label]=" 'item. edit bitstreams.edit.buttons.remove' | translate" [attr.aria-label]=" 'item. edit bitstreams.edit.buttons.remove' | translate"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button [disabled]="!canUndo(update)" (click)="undo(entry.bitstream)" <button [dsBtnDisabled]="!canUndo(update)" (click)="undo(entry.bitstream)"
[attr.aria-label]="'item.edit.bitstreams.edit.buttons.undo' | translate" [attr.aria-label]="'item.edit.bitstreams.edit.buttons.undo' | translate"
class="btn btn-outline-warning btn-sm" class="btn btn-outline-warning btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}">

View File

@@ -52,6 +52,7 @@ import {
getAllSucceededRemoteData, getAllSucceededRemoteData,
paginatedListToArray, paginatedListToArray,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -87,6 +88,7 @@ import {
NgbDropdownModule, NgbDropdownModule,
CdkDrag, CdkDrag,
BrowserOnlyPipe, BrowserOnlyPipe,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -86,10 +86,10 @@
</ng-container> </ng-container>
<div class="space-children-mr"> <div class="space-children-mr">
<button [disabled]="isDeleting$ | async" (click)="performAction()" <button [dsBtnDisabled]="isDeleting$ | async" (click)="performAction()"
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}} class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button> </button>
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']" <button [dsBtnDisabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
class="btn btn-outline-secondary cancel"> class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}} {{cancelMessage| translate}}
</button> </button>

View File

@@ -56,6 +56,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { ViewMode } from '../../../core/shared/view-mode.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -109,6 +110,7 @@ class RelationshipDTO {
VarDirective, VarDirective,
NgForOf, NgForOf,
RouterLink, RouterLink,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -40,7 +40,7 @@
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary"> <button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}} <i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
</button> </button>
<button class="btn btn-primary" [disabled]="!canMove" (click)="moveToCollection()"> <button class="btn btn-primary" [dsBtnDisabled]="!canMove" (click)="moveToCollection()">
<span *ngIf="!processing"> <span *ngIf="!processing">
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}} <i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
</span> </span>
@@ -48,7 +48,7 @@
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}} <i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
</span> </span>
</button> </button>
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()"> <button class="btn btn-danger" [dsBtnDisabled]="!canSubmit" (click)="discard()">
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}} <i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
</button> </button>
</div> </div>

View File

@@ -37,6 +37,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -56,6 +57,7 @@ import {
AsyncPipe, AsyncPipe,
AuthorizedCollectionSelectorComponent, AuthorizedCollectionSelectorComponent,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -5,12 +5,12 @@
</div> </div>
<div class="col-12 col-md-9 float-left action-button"> <div class="col-12 col-md-9 float-left action-button">
<span *ngIf="operation.authorized"> <span *ngIf="operation.authorized">
<button class="btn btn-outline-primary" [disabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate"> <button class="btn btn-outline-primary" [dsBtnDisabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</button> </button>
</span> </span>
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate"> <span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
<button class="btn btn-outline-primary" [disabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate"> <button class="btn btn-outline-primary" [dsBtnDisabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</button> </button>
</span> </span>

View File

@@ -6,6 +6,7 @@ import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ItemOperationComponent } from './item-operation.component'; import { ItemOperationComponent } from './item-operation.component';
import { ItemOperation } from './itemOperation.model'; import { ItemOperation } from './itemOperation.model';
@@ -17,7 +18,7 @@ describe('ItemOperationComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent, BtnDisabledDirective],
}).compileComponents(); }).compileComponents();
})); }));
@@ -43,7 +44,8 @@ describe('ItemOperationComponent', () => {
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
const button = fixture.debugElement.query(By.css('button')).nativeElement; const button = fixture.debugElement.query(By.css('button')).nativeElement;
expect(button.disabled).toBeTrue(); expect(button.getAttribute('aria-disabled')).toBe('true');
expect(button.classList.contains('disabled')).toBeTrue();
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
}); });
}); });

View File

@@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ItemOperation } from './itemOperation.model'; import { ItemOperation } from './itemOperation.model';
@Component({ @Component({
@@ -17,6 +18,7 @@ import { ItemOperation } from './itemOperation.model';
RouterLink, RouterLink,
NgbTooltipModule, NgbTooltipModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,6 +1,6 @@
<h2 class="h4"> <h2 class="h4">
{{relationshipMessageKey$ | async | translate}} {{relationshipMessageKey$ | async | translate}}
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()"> <button class="ml-2 btn btn-success" [dsBtnDisabled]="(hasChanges | async)" (click)="openLookup()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.relationships.edit.buttons.add" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.relationships.edit.buttons.add" | translate}}</span>
</button> </button>

View File

@@ -388,7 +388,8 @@ describe('EditRelationshipListComponent', () => {
comp.hasChanges = observableOf(true); comp.hasChanges = observableOf(true);
fixture.detectChanges(); fixture.detectChanges();
const element = de.query(By.css('.btn-success')); const element = de.query(By.css('.btn-success'));
expect(element.nativeElement?.disabled).toBeTrue(); expect(element.nativeElement?.getAttribute('aria-disabled')).toBe('true');
expect(element.nativeElement?.classList.contains('disabled')).toBeTrue();
}); });
}); });

View File

@@ -65,6 +65,7 @@ import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -100,6 +101,7 @@ import { EditRelationshipComponent } from '../edit-relationship/edit-relationshi
TranslateModule, TranslateModule,
NgClass, NgClass,
ThemedLoadingComponent, ThemedLoadingComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -9,12 +9,12 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<div class="btn-group relationship-action-buttons"> <div class="btn-group relationship-action-buttons">
<button [disabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)" <button [dsBtnDisabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}"> title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button [disabled]="!canUndo()" (click)="undo()" <button [dsBtnDisabled]="!canUndo()" (click)="undo()"
class="btn btn-outline-warning btn-sm" class="btn btn-outline-warning btn-sm"
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}"> title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i> <i class="fas fa-undo-alt fa-fw"></i>

View File

@@ -37,6 +37,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -54,6 +55,7 @@ import { VirtualMetadataComponent } from '../../virtual-metadata/virtual-metadat
NgIf, NgIf,
TranslateModule, TranslateModule,
VirtualMetadataComponent, VirtualMetadataComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -35,7 +35,7 @@
<ng-template #buttons> <ng-template #buttons>
<div class="d-flex space-children-mr justify-content-end"> <div class="d-flex space-children-mr justify-content-end">
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"> (click)="discard()">
<i aria-hidden="true" class="fas fa-times"></i> <i aria-hidden="true" class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span>
@@ -45,7 +45,7 @@
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.reinstate-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.reinstate-button' | translate }}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true" [dsBtnDisabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
(click)="submit()"> (click)="submit()">
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span> <span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i> <i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>

View File

@@ -42,6 +42,7 @@ import {
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type'; import { AlertType } from '../../../shared/alert/alert-type';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -67,6 +68,7 @@ import { EditRelationshipListWrapperComponent } from './edit-relationship-list-w
TranslateModule, TranslateModule,
VarDirective, VarDirective,
EditRelationshipListWrapperComponent, EditRelationshipListWrapperComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -19,7 +19,7 @@
<div class="buttons" *ngIf="medias?.length > 1"> <div class="buttons" *ngIf="medias?.length > 1">
<button <button
class="btn btn-primary previous" class="btn btn-primary previous"
[disabled]="currentIndex === 0" [dsBtnDisabled]="currentIndex === 0"
(click)="prevMedia()" (click)="prevMedia()"
> >
{{ "media-viewer.previous" | translate }} {{ "media-viewer.previous" | translate }}
@@ -27,7 +27,7 @@
<button <button
class="btn btn-primary next" class="btn btn-primary next"
[disabled]="currentIndex === medias.length - 1" [dsBtnDisabled]="currentIndex === medias.length - 1"
(click)="nextMedia()" (click)="nextMedia()"
> >
{{ "media-viewer.next" | translate }} {{ "media-viewer.next" | translate }}

View File

@@ -12,6 +12,7 @@ import { Bitstream } from 'src/app/core/shared/bitstream.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { CaptionInfo } from './caption-info'; import { CaptionInfo } from './caption-info';
import { languageHelper } from './language-helper'; import { languageHelper } from './language-helper';
@@ -27,6 +28,7 @@ import { languageHelper } from './language-helper';
NgbDropdownModule, NgbDropdownModule,
TranslateModule, TranslateModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -48,7 +48,7 @@
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner"> <div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
<div class="col"> <div class="col">
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()" <button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
[disabled]="(unlinkProcessing | async)"> [dsBtnDisabled]="(unlinkProcessing | async)">
<span *ngIf="(unlinkProcessing | async) !== true"><i <span *ngIf="(unlinkProcessing | async) !== true"><i
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span> class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
<span *ngIf="(unlinkProcessing | async)"><i <span *ngIf="(unlinkProcessing | async)"><i

View File

@@ -34,6 +34,7 @@ import {
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
@@ -47,6 +48,7 @@ import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-d
NgIf, NgIf,
NgForOf, NgForOf,
AlertComponent, AlertComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -31,7 +31,7 @@
<!--CREATE--> <!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async"> <ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create" <button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || hasDraftVersion" [dsBtnDisabled]="isAnyBeingEdited() || hasDraftVersion"
(click)="createNewVersion(version)" (click)="createNewVersion(version)"
title="{{createVersionTitle | translate }}"> title="{{createVersionTitle | translate }}">
<i class="fas fa-code-branch fa-fw"></i> <i class="fas fa-code-branch fa-fw"></i>
@@ -41,7 +41,7 @@
<ng-container *ngIf="canDeleteVersion$ | async"> <ng-container *ngIf="canDeleteVersion$ | async">
<button class="btn btn-sm version-row-element-delete" <button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'" [ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()" [dsBtnDisabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id === itemVersion.id)" (click)="deleteVersion(version, version.id === itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}"> title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i> <i class="fas fa-trash fa-fw"></i>

View File

@@ -49,6 +49,7 @@ import { VersionHistory } from '../../../core/shared/version-history.model';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { import {
getItemEditVersionhistoryRoute, getItemEditVersionhistoryRoute,
@@ -67,6 +68,7 @@ import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-moda
TranslateModule, TranslateModule,
NgClass, NgClass,
NgIf, NgIf,
BtnDisabledDirective,
], ],
templateUrl: './item-versions-row-element-version.component.html', templateUrl: './item-versions-row-element-version.component.html',
styleUrl: './item-versions-row-element-version.component.scss', styleUrl: './item-versions-row-element-version.component.scss',

View File

@@ -67,7 +67,7 @@
<ng-template #notThisBeingEdited> <ng-template #notThisBeingEdited>
<!--EDIT--> <!--EDIT-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit" <button class="btn btn-outline-primary btn-sm version-row-element-edit"
[disabled]="isAnyBeingEdited()" [dsBtnDisabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(versionDTO.version)" (click)="enableVersionEditing(versionDTO.version)"
title="{{'item.version.history.table.action.editSummary' | translate}}"> title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>

View File

@@ -42,6 +42,7 @@ import { VersionHistory } from '../../core/shared/version-history.model';
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
@@ -158,7 +159,7 @@ describe('ItemVersionsComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective, BtnDisabledDirective],
providers: [ providers: [
{ provide: PaginationService, useValue: new PaginationServiceStub() }, { provide: PaginationService, useValue: new PaginationServiceStub() },
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() }, { provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
@@ -234,8 +235,9 @@ describe('ItemVersionsComponent', () => {
it('should not disable the delete button', () => { it('should not disable the delete button', () => {
const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete')); const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete'));
expect(deleteButtons.length).not.toBe(0); expect(deleteButtons.length).not.toBe(0);
deleteButtons.forEach((btn: DebugElement) => { deleteButtons.forEach((btn) => {
expect(btn.nativeElement.disabled).toBe(false); expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });

View File

@@ -50,6 +50,7 @@ import { Version } from '../../core/shared/version.model';
import { VersionHistory } from '../../core/shared/version-history.model'; import { VersionHistory } from '../../core/shared/version-history.model';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
hasValueOperator, hasValueOperator,
@@ -78,7 +79,7 @@ interface VersionDTO {
templateUrl: './item-versions.component.html', templateUrl: './item-versions.component.html',
styleUrls: ['./item-versions.component.scss'], styleUrls: ['./item-versions.component.scss'],
standalone: true, standalone: true,
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent], imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent, BtnDisabledDirective],
}) })
/** /**

View File

@@ -1,6 +1,6 @@
<div class="add" *ngIf="(moreThanOne$ | async) !== true"> <div class="add" *ngIf="(moreThanOne$ | async) !== true">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" <button class="btn btn-lg btn-outline-primary mt-1 ml-2"
[attr.aria-label]="'mydspace.new-submission-external' | translate" [disabled]="(initialized$ | async) !== true" [attr.aria-label]="'mydspace.new-submission-external' | translate" [dsBtnDisabled]="(initialized$ | async) !== true"
(click)="openPage(singleEntity)" role="button" (click)="openPage(singleEntity)" role="button"
title="{{'mydspace.new-submission-external' | translate}}"> title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i> <i class="fa fa-file-import" aria-hidden="true"></i>
@@ -10,7 +10,7 @@
ngbDropdown ngbDropdown
*ngIf="(moreThanOne$ | async)"> *ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle <button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
type="button" [disabled]="(initialized$ | async) !== true" type="button" [dsBtnDisabled]="(initialized$ | async) !== true"
[attr.aria-label]="'mydspace.new-submission-external' | translate" [attr.aria-label]="'mydspace.new-submission-external' | translate"
[attr.data-test]="'import-dropdown' | dsBrowserOnly" [attr.data-test]="'import-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission-external' | translate}}"> title="{{'mydspace.new-submission-external' | translate}}">

View File

@@ -27,6 +27,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component'; import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
@@ -45,6 +46,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
TranslateModule, TranslateModule,
BrowserOnlyPipe, BrowserOnlyPipe,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,6 +1,6 @@
<div class="add" *ngIf="(moreThanOne$ | async) !== true"> <div class="add" *ngIf="(moreThanOne$ | async) !== true">
<button class="btn btn-lg btn-primary mt-1 ml-2" [attr.aria-label]="'mydspace.new-submission' | translate" <button class="btn btn-lg btn-primary mt-1 ml-2" [attr.aria-label]="'mydspace.new-submission' | translate"
[disabled]="(initialized$ | async) !== true" (click)="openDialog(singleEntity)" role="button"> [dsBtnDisabled]="(initialized$ | async) !== true" (click)="openDialog(singleEntity)" role="button">
<i class="fa fa-plus-circle" aria-hidden="true"></i> <i class="fa fa-plus-circle" aria-hidden="true"></i>
</button> </button>
</div> </div>
@@ -8,7 +8,7 @@
ngbDropdown ngbDropdown
*ngIf="(moreThanOne$ | async)"> *ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle <button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
type="button" [disabled]="(initialized$ | async) !== true" type="button" [dsBtnDisabled]="(initialized$ | async) !== true"
[attr.aria-label]="'mydspace.new-submission' | translate" [attr.aria-label]="'mydspace.new-submission' | translate"
[attr.data-test]="'submission-dropdown' | dsBrowserOnly" [attr.data-test]="'submission-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission' | translate}}"> title="{{'mydspace.new-submission' | translate}}">

View File

@@ -28,6 +28,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; import { ThemedCreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component'; import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
@@ -47,6 +48,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
TranslateModule, TranslateModule,
BrowserOnlyPipe, BrowserOnlyPipe,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -138,7 +138,7 @@
<div class="btn-group"> <div class="btn-group">
<button *ngIf="!eventElement.hasProject" <button *ngIf="!eventElement.hasProject"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="openModalLookup(eventElement); $event.stopPropagation();" (click)="openModalLookup(eventElement); $event.stopPropagation();"
[attr.aria-label]="'quality-assurance.event.modal.project.select' | translate" [attr.aria-label]="'quality-assurance.event.modal.project.select' | translate"
> >
@@ -146,7 +146,7 @@
</button> </button>
<button *ngIf="eventElement.hasProject" <button *ngIf="eventElement.hasProject"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="removeProject(eventElement)" (click)="removeProject(eventElement)"
[attr.aria-label]="'quality-assurance.event.modal.project.remove' | translate" [attr.aria-label]="'quality-assurance.event.modal.project.remove' | translate"
> >
@@ -161,7 +161,7 @@
class="btn btn-outline-success btn-sm button-width" class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.import' | translate}}" ngbTooltip="{{'quality-assurance.event.action.import' | translate}}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="modalChoice('ACCEPTED', eventElement, acceptModal)" (click)="modalChoice('ACCEPTED', eventElement, acceptModal)"
[attr.aria-label]="'quality-assurance.event.action.import' | translate" [attr.aria-label]="'quality-assurance.event.action.import' | translate"
> >
@@ -171,7 +171,7 @@
class="btn btn-outline-success btn-sm button-width" class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.accept' | translate}}" ngbTooltip="{{'quality-assurance.event.action.accept' | translate}}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="executeAction('ACCEPTED', eventElement)" (click)="executeAction('ACCEPTED', eventElement)"
[attr.aria-label]="'quality-assurance.event.action.accept' | translate" [attr.aria-label]="'quality-assurance.event.action.accept' | translate"
> >
@@ -180,7 +180,7 @@
<button class="btn btn-outline-dark btn-sm button-width" <button class="btn btn-outline-dark btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.ignore' | translate}}" ngbTooltip="{{'quality-assurance.event.action.ignore' | translate}}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="openModal('DISCARDED', eventElement, ignoreModal)" (click)="openModal('DISCARDED', eventElement, ignoreModal)"
[attr.aria-label]="'quality-assurance.event.action.ignore' | translate" [attr.aria-label]="'quality-assurance.event.action.ignore' | translate"
> >
@@ -190,7 +190,7 @@
*ngIf="(isAdmin$ | async)" *ngIf="(isAdmin$ | async)"
ngbTooltip="{{'quality-assurance.event.action.reject' | translate}}" ngbTooltip="{{'quality-assurance.event.action.reject' | translate}}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
(click)="openModal('REJECTED', eventElement, rejectModal)" (click)="openModal('REJECTED', eventElement, rejectModal)"
[attr.aria-label]="'quality-assurance.event.action.reject' | translate" [attr.aria-label]="'quality-assurance.event.action.reject' | translate"
> >
@@ -200,7 +200,7 @@
*ngIf="(isAdmin$ | async) === false" *ngIf="(isAdmin$ | async) === false"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate }}" ngbTooltip="{{'quality-assurance.event.action.undo' | translate }}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate" [attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)"> (click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
@@ -210,7 +210,7 @@
<button class="btn btn-outline-danger btn-sm button-width" <button class="btn btn-outline-danger btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate}}" ngbTooltip="{{'quality-assurance.event.action.undo' | translate}}"
container="body" container="body"
[disabled]="eventElement.isRunning" [dsBtnDisabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate" [attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)"> (click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>

View File

@@ -65,6 +65,7 @@ import {
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -86,7 +87,7 @@ import { EPersonDataComponent } from './ePerson-data/ePerson-data.component';
templateUrl: './quality-assurance-events.component.html', templateUrl: './quality-assurance-events.component.html',
styleUrls: ['./quality-assurance-events.component.scss'], styleUrls: ['./quality-assurance-events.component.scss'],
standalone: true, standalone: true,
imports: [AlertComponent, NgIf, ThemedLoadingComponent, PaginationComponent, NgFor, RouterLink, NgbTooltipModule, AsyncPipe, TranslateModule, EPersonDataComponent], imports: [AlertComponent, NgIf, ThemedLoadingComponent, PaginationComponent, NgFor, RouterLink, NgbTooltipModule, AsyncPipe, TranslateModule, EPersonDataComponent, BtnDisabledDirective],
}) })
export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
/** /**

View File

@@ -32,8 +32,8 @@
<div id="project-search" class="input-group mb-3"> <div id="project-search" class="input-group mb-3">
<input type="text" class="form-control" (keyup.enter)="search(projectTitle)" [(ngModel)]="projectTitle" placeholder="{{labelPrefix + label + '.placeholder' |translate}}" aria-label="" aria-describedby=""> <input type="text" class="form-control" (keyup.enter)="search(projectTitle)" [(ngModel)]="projectTitle" placeholder="{{labelPrefix + label + '.placeholder' |translate}}" aria-label="" aria-describedby="">
<div class="input-group-append"> <div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" [disabled]="projectTitle === ''" (click)="projectTitle = ''">{{(labelPrefix + label + '.clear'|translate)}}</button> <button type="button" class="btn btn-outline-secondary" [dsBtnDisabled]="projectTitle === ''" (click)="projectTitle = ''">{{(labelPrefix + label + '.clear'|translate)}}</button>
<button type="button" class="btn btn-primary" [disabled]="projectTitle === ''" (click)="search(projectTitle)">{{(labelPrefix + label + '.search'|translate)}}</button> <button type="button" class="btn btn-primary" [dsBtnDisabled]="projectTitle === ''" (click)="search(projectTitle)">{{(labelPrefix + label + '.search'|translate)}}</button>
</div> </div>
</div> </div>
@@ -66,6 +66,6 @@
<button type="button" class="btn btn-outline-secondary" (click)="close()">{{ (labelPrefix + label + '.cancel' | translate) }}</button> <button type="button" class="btn btn-outline-secondary" (click)="close()">{{ (labelPrefix + label + '.cancel' | translate) }}</button>
</div> </div>
<div> <div>
<button type="button" class="btn btn-primary" [disabled]="selectedImportType === importType.None" (click)="bound()">{{ (labelPrefix + label + '.bound' | translate) }}</button> <button type="button" class="btn btn-primary" [dsBtnDisabled]="selectedImportType === importType.None" (click)="bound()">{{ (labelPrefix + label + '.bound' | translate) }}</button>
</div> </div>
</div> </div>

View File

@@ -30,6 +30,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -105,7 +106,7 @@ export interface QualityAssuranceEventData {
styleUrls: ['./project-entry-import-modal.component.scss'], styleUrls: ['./project-entry-import-modal.component.scss'],
templateUrl: './project-entry-import-modal.component.html', templateUrl: './project-entry-import-modal.component.html',
standalone: true, standalone: true,
imports: [RouterLink, NgIf, FormsModule, ThemedLoadingComponent, ThemedSearchResultsComponent, AlertComponent, AsyncPipe, TranslateModule], imports: [RouterLink, NgIf, FormsModule, ThemedLoadingComponent, ThemedSearchResultsComponent, AlertComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component to display a modal window for linking a project to an Quality Assurance event * Component to display a modal window for linking a project to an Quality Assurance event

View File

@@ -21,7 +21,7 @@
</div> </div>
<button (click)="ignoreSuggestion()" class="btn btn-danger ml-2"><i class="fa fa-ban"></i> <button (click)="ignoreSuggestion()" class="btn btn-danger ml-2"><i class="fa fa-ban"></i>
{{ ignoreSuggestionLabel() | translate}}</button> {{ ignoreSuggestionLabel() | translate}}</button>
<button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [disabled]="!hasEvidence" class="btn btn-info ml-2"> <button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [dsBtnDisabled]="!hasEvidence" class="btn btn-info ml-2">
<i class="fa fa-eye"></i> <i class="fa fa-eye"></i>
<ng-container *ngIf="!seeEvidence"> {{ 'suggestion.seeEvidence' | translate}}</ng-container> <ng-container *ngIf="!seeEvidence"> {{ 'suggestion.seeEvidence' | translate}}</ng-container>
<ng-container *ngIf="seeEvidence"> {{ 'suggestion.hideEvidence' | translate}}</ng-container> <ng-container *ngIf="seeEvidence"> {{ 'suggestion.hideEvidence' | translate}}</ng-container>

View File

@@ -15,6 +15,7 @@ import { take } from 'rxjs/operators';
import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model'; import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { ItemType } from '../../core/shared/item-relationships/item-type.model'; import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; import { ThemedCreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { EntityDropdownComponent } from '../../shared/entity-dropdown/entity-dropdown.component'; import { EntityDropdownComponent } from '../../shared/entity-dropdown/entity-dropdown.component';
import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-approve-and-import'; import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-approve-and-import';
@@ -31,6 +32,7 @@ import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestio
TranslateModule, TranslateModule,
NgIf, NgIf,
NgbDropdownModule, NgbDropdownModule,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -64,10 +64,10 @@
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span> <span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async" <button class="btn btn-primary mr-2" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button> (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger" <button id="delete-confirm" class="btn btn-danger"
[disabled]="processBulkDeleteService.isProcessing$() |async" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }} (click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button> </button>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,7 +32,7 @@ import { ProcessOverviewTableComponent } from './table/process-overview-table.co
selector: 'ds-process-overview', selector: 'ds-process-overview',
templateUrl: './process-overview.component.html', templateUrl: './process-overview.component.html',
standalone: true, standalone: true,
imports: [NgIf, RouterLink, PaginationComponent, NgFor, VarDirective, AsyncPipe, DatePipe, TranslateModule, NgTemplateOutlet, ProcessOverviewTableComponent], imports: [NgIf, RouterLink, PaginationComponent, NgFor, VarDirective, AsyncPipe, DatePipe, TranslateModule, NgTemplateOutlet, ProcessOverviewTableComponent, BtnDisabledDirective],
}) })
/** /**
* Component displaying a list of all processes in a paginated table * Component displaying a list of all processes in a paginated table

View File

@@ -29,7 +29,7 @@
<input type="checkbox" [checked]="checked" (change)="toggleCheckbox()"/> <input type="checkbox" [checked]="checked" (change)="toggleCheckbox()"/>
{{ 'dso-selector.claim.item.not-mine-label' | translate }} {{ 'dso-selector.claim.item.not-mine-label' | translate }}
</div> </div>
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [disabled]="!checked"> <button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [dsBtnDisabled]="!checked">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{ 'dso-selector.claim.item.create-from-scratch' | translate }} {{ 'dso-selector.claim.item.create-from-scratch' | translate }}
</button> </button>

View File

@@ -25,6 +25,7 @@ import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DSOSelectorModalWrapperComponent } from '../../shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component'; import { DSOSelectorModalWrapperComponent } from '../../shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component';
import { CollectionElementLinkType } from '../../shared/object-collection/collection-element-link.type'; import { CollectionElementLinkType } from '../../shared/object-collection/collection-element-link.type';
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
@@ -42,6 +43,7 @@ import { ProfileClaimService } from '../profile-claim/profile-claim.service';
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -13,7 +13,7 @@
<p>{{'researcher.profile.not.associated' | translate}}</p> <p>{{'researcher.profile.not.associated' | translate}}</p>
</div> </div>
<button *ngIf="!researcherProfile" class="btn btn-primary mr-2" <button *ngIf="!researcherProfile" class="btn btn-primary mr-2"
[disabled]="(isProcessingCreate() | async)" [dsBtnDisabled]="(isProcessingCreate() | async)"
(click)="createProfile()"> (click)="createProfile()">
<span *ngIf="(isProcessingCreate() | async)"> <span *ngIf="(isProcessingCreate() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
@@ -23,10 +23,10 @@
</span> </span>
</button> </button>
<ng-container *ngIf="researcherProfile"> <ng-container *ngIf="researcherProfile">
<button class="btn btn-primary mr-2" [disabled]="!researcherProfile" (click)="viewProfile(researcherProfile)"> <button class="btn btn-primary mr-2" [dsBtnDisabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}} <i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
</button> </button>
<button class="btn btn-danger" [disabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)"> <button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
<span *ngIf="(isProcessingDelete() | async)"> <span *ngIf="(isProcessingDelete() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span> </span>

View File

@@ -36,6 +36,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -53,6 +54,7 @@ import { ProfileClaimItemModalComponent } from '../profile-claim-item-modal/prof
TranslateModule, TranslateModule,
UiSwitchModule, UiSwitchModule,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -51,13 +51,13 @@
</div> </div>
<ng-container *ngIf="(!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible')); else v2Invisible"> <ng-container *ngIf="(!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible')); else v2Invisible">
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()"> <button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
{{ MESSAGE_PREFIX + '.submit' | translate }} {{ MESSAGE_PREFIX + '.submit' | translate }}
</button> </button>
</ng-container> </ng-container>
<ng-template #v2Invisible> <ng-template #v2Invisible>
<button class="btn btn-primary" [disabled]="form.invalid" (click)="executeRecaptcha()"> <button class="btn btn-primary" [dsBtnDisabled]="form.invalid" (click)="executeRecaptcha()">
{{ MESSAGE_PREFIX + '.submit' | translate }} {{ MESSAGE_PREFIX + '.submit' | translate }}
</button> </button>
</ng-template> </ng-template>

View File

@@ -54,6 +54,7 @@ import {
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
import { AlertComponent } from '../shared/alert/alert.component'; import { AlertComponent } from '../shared/alert/alert.component';
import { AlertType } from '../shared/alert/alert-type'; import { AlertType } from '../shared/alert/alert-type';
import { BtnDisabledDirective } from '../shared/btn-disabled.directive';
import { KlaroService } from '../shared/cookies/klaro.service'; import { KlaroService } from '../shared/cookies/klaro.service';
import { isNotEmpty } from '../shared/empty.util'; import { isNotEmpty } from '../shared/empty.util';
import { GoogleRecaptchaComponent } from '../shared/google-recaptcha/google-recaptcha.component'; import { GoogleRecaptchaComponent } from '../shared/google-recaptcha/google-recaptcha.component';
@@ -66,7 +67,7 @@ export const TYPE_REQUEST_REGISTER = 'register';
selector: 'ds-base-register-email-form', selector: 'ds-base-register-email-form',
templateUrl: './register-email-form.component.html', templateUrl: './register-email-form.component.html',
standalone: true, standalone: true,
imports: [NgIf, FormsModule, ReactiveFormsModule, AlertComponent, GoogleRecaptchaComponent, AsyncPipe, TranslateModule], imports: [NgIf, FormsModule, ReactiveFormsModule, AlertComponent, GoogleRecaptchaComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component responsible to render an email registration form. * Component responsible to render an email registration form.

View File

@@ -81,7 +81,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<button <button
[disabled]="isInValidPassword || userInfoForm.invalid" [dsBtnDisabled]="isInValidPassword || userInfoForm.invalid"
class="btn btn-default btn-primary" class="btn btn-default btn-primary"
(click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button> (click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button>
</div> </div>

View File

@@ -43,6 +43,7 @@ import {
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component'; import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { isEmpty } from '../../shared/empty.util'; import { isEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -60,6 +61,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
AsyncPipe, AsyncPipe,
ReactiveFormsModule, ReactiveFormsModule,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -13,7 +13,7 @@
<ng-content></ng-content> <ng-content></ng-content>
<div class="d-flex flex-row-reverse"> <div class="d-flex flex-row-reverse">
<button (click)="submit()" <button (click)="submit()"
[disabled]="!subject || subject.length === 0" [dsBtnDisabled]="!subject || subject.length === 0"
class="btn btn-primary" class="btn btn-primary"
title="{{'grant-deny-request-copy.email.send' | translate }}"> title="{{'grant-deny-request-copy.email.send' | translate }}">
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }} <i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}

View File

@@ -12,6 +12,7 @@ import {
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { RequestCopyEmail } from './request-copy-email.model'; import { RequestCopyEmail } from './request-copy-email.model';
@@ -20,7 +21,7 @@ import { RequestCopyEmail } from './request-copy-email.model';
styleUrls: ['./email-request-copy.component.scss'], styleUrls: ['./email-request-copy.component.scss'],
templateUrl: './email-request-copy.component.html', templateUrl: './email-request-copy.component.html',
standalone: true, standalone: true,
imports: [FormsModule, NgClass, NgIf, TranslateModule], imports: [FormsModule, NgClass, NgIf, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* A form component for an email to send back to the user requesting an item * A form component for an email to send back to the user requesting an item

View File

@@ -47,7 +47,7 @@
<div class="input-group-append"> <div class="input-group-append">
<button <button
class="btn btn-outline-secondary fas fa-calendar" class="btn btn-outline-secondary fas fa-calendar"
[disabled]="ngForm.disabled" [dsBtnDisabled]="ngForm.disabled"
(click)="d.toggle()" type="button"> (click)="d.toggle()" type="button">
</button> </button>
</div> </div>
@@ -76,7 +76,7 @@
<div class="input-group-append"> <div class="input-group-append">
<button <button
type="button" class="btn btn-outline-secondary fas fa-calendar" type="button" class="btn btn-outline-secondary fas fa-calendar"
[disabled]="ngForm.disabled" [dsBtnDisabled]="ngForm.disabled"
(click)="d1.toggle()"> (click)="d1.toggle()">
</button> </button>
</div> </div>
@@ -93,7 +93,7 @@
<button type="button" class="btn btn-outline-danger" <button type="button" class="btn btn-outline-danger"
[attr.aria-label]="'access-control-remove' | translate" [attr.aria-label]="'access-control-remove' | translate"
[disabled]="ngForm.disabled || form.accessControls.length === 1" [dsBtnDisabled]="ngForm.disabled || form.accessControls.length === 1"
(click)="removeAccessControlItem(control.id)"> (click)="removeAccessControlItem(control.id)">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@@ -103,7 +103,7 @@
</ng-container> </ng-container>
<button type="button" id="add-btn-{{type}}" class="btn btn-outline-primary mt-3" <button type="button" id="add-btn-{{type}}" class="btn btn-outline-primary mt-3"
[disabled]="ngForm.disabled" [dsBtnDisabled]="ngForm.disabled"
(click)="addAccessControlItem()"> (click)="addAccessControlItem()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{'access-control-add-more' | translate}} {{'access-control-add-more' | translate}}

View File

@@ -16,6 +16,7 @@ import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model'; import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
import { BtnDisabledDirective } from '../../btn-disabled.directive';
import { dateToISOFormat } from '../../date.util'; import { dateToISOFormat } from '../../date.util';
import { ToDatePipe } from './to-date.pipe'; import { ToDatePipe } from './to-date.pipe';
@@ -25,7 +26,7 @@ import { ToDatePipe } from './to-date.pipe';
styleUrls: ['./access-control-array-form.component.scss'], styleUrls: ['./access-control-array-form.component.scss'],
exportAs: 'accessControlArrayForm', exportAs: 'accessControlArrayForm',
standalone: true, standalone: true,
imports: [FormsModule, NgIf, NgFor, NgbDatepickerModule, TranslateModule, ToDatePipe], imports: [FormsModule, NgIf, NgFor, NgbDatepickerModule, TranslateModule, ToDatePipe, BtnDisabledDirective],
}) })
export class AccessControlArrayFormComponent implements OnInit { export class AccessControlArrayFormComponent implements OnInit {
@Input() dropdownOptions: AccessesConditionOption[] = []; @Input() dropdownOptions: AccessesConditionOption[] = [];

View File

@@ -103,7 +103,7 @@
<button <button
*ngIf="itemRD" *ngIf="itemRD"
[attr.aria-label]="'access-control-bitstreams-select' | translate" [attr.aria-label]="'access-control-bitstreams-select' | translate"
[disabled]="!state.bitstream.toggleStatus || state.bitstream.changesLimit !== 'selected'" [dsBtnDisabled]="!state.bitstream.toggleStatus || state.bitstream.changesLimit !== 'selected'"
(click)="openSelectBitstreamsModal(itemRD.payload)" (click)="openSelectBitstreamsModal(itemRD.payload)"
class="btn btn-outline-dark border-0" type="button"> class="btn btn-outline-dark border-0" type="button">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
@@ -161,7 +161,7 @@
{{ 'access-control-cancel' | translate }} {{ 'access-control-cancel' | translate }}
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus" [dsBtnDisabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus"
(click)="submit()" type="submit"> (click)="submit()" type="submit">
{{ 'access-control-execute' | translate }} {{ 'access-control-execute' | translate }}
</button> </button>

View File

@@ -31,6 +31,7 @@ import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { AlertComponent } from '../alert/alert.component'; import { AlertComponent } from '../alert/alert.component';
import { AlertType } from '../alert/alert-type'; import { AlertType } from '../alert/alert-type';
import { BtnDisabledDirective } from '../btn-disabled.directive';
import { SelectableListService } from '../object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../object-list/selectable-list/selectable-list.service';
import { AccessControlArrayFormComponent } from './access-control-array-form/access-control-array-form.component'; import { AccessControlArrayFormComponent } from './access-control-array-form/access-control-array-form.component';
import { createAccessControlInitialFormState } from './access-control-form-container-intial-state'; import { createAccessControlInitialFormState } from './access-control-form-container-intial-state';
@@ -46,7 +47,7 @@ import {
styleUrls: ['./access-control-form-container.component.scss'], styleUrls: ['./access-control-form-container.component.scss'],
exportAs: 'dsAccessControlForm', exportAs: 'dsAccessControlForm',
standalone: true, standalone: true,
imports: [NgIf, AlertComponent, UiSwitchModule, FormsModule, AccessControlArrayFormComponent, AsyncPipe, TranslateModule], imports: [NgIf, AlertComponent, UiSwitchModule, FormsModule, AccessControlArrayFormComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
}) })
export class AccessControlFormContainerComponent<T extends DSpaceObject> implements OnDestroy { export class AccessControlFormContainerComponent<T extends DSpaceObject> implements OnDestroy {

View File

@@ -0,0 +1,63 @@
import {
Directive,
HostBinding,
HostListener,
Input,
} from '@angular/core';
@Directive({
selector: '[dsBtnDisabled]',
standalone: true,
})
/**
* This directive can be added to a html element to disable it (make it non-interactive).
* It acts as a replacement for HTML's disabled attribute.
*
* This directive should always be used instead of the HTML disabled attribute as it is more accessible.
*/
export class BtnDisabledDirective {
@Input() set dsBtnDisabled(value: boolean) {
this.isDisabled = !!value;
}
/**
* Binds the aria-disabled attribute to the directive's isDisabled property.
* This is used to make the element accessible to screen readers. If the element is disabled, the screen reader will announce it as such.
*/
@HostBinding('attr.aria-disabled') isDisabled = false;
/**
* Binds the class attribute to the directive's isDisabled property.
* This is used to style the element when it is disabled (make it look disabled).
*/
@HostBinding('class.disabled') get disabledClass() { return this.isDisabled; }
/**
* Prevents the default action and stops the event from propagating when the element is disabled.
* This is used to prevent the element from being interacted with when it is disabled (via mouse click).
* @param event The click event.
*/
@HostListener('click', ['$event'])
handleClick(event: Event) {
if (this.isDisabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
/**
* Prevents the default action and stops the event from propagating when the element is disabled.
* This is used to prevent the element from being interacted with when it is disabled (via keystrokes).
* @param event The keydown event.
*/
@HostListener('keydown', ['$event'])
handleKeydown(event: KeyboardEvent) {
if (this.isDisabled && (event.key === 'Enter' || event.key === 'Space')) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
}

View File

@@ -0,0 +1,105 @@
import {
Component,
DebugElement,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BtnDisabledDirective } from './btn-disabled.directive';
@Component({
template: `
<button [dsBtnDisabled]="isDisabled">Test Button</button>
`,
})
class TestComponent {
isDisabled = false;
}
describe('DisabledDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let button: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BtnDisabledDirective],
declarations: [TestComponent],
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
button = fixture.debugElement.query(By.css('button'));
fixture.detectChanges();
});
it('should bind aria-disabled to false initially', () => {
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
});
it('should bind aria-disabled to true and add disabled class when isDisabled is true', () => {
component.isDisabled = true;
fixture.detectChanges();
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
});
it('should bind aria-disabled to false and not have disabled class when isDisabled is false', () => {
component.isDisabled = false;
fixture.detectChanges();
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
});
it('should prevent click events when disabled', () => {
component.isDisabled = true;
fixture.detectChanges();
let clickHandled = false;
button.nativeElement.addEventListener('click', () => clickHandled = true);
button.nativeElement.click();
expect(clickHandled).toBeFalse();
});
it('should prevent Enter or Space keydown events when disabled', () => {
component.isDisabled = true;
fixture.detectChanges();
let keydownHandled = false;
button.nativeElement.addEventListener('keydown', () => keydownHandled = true);
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
button.nativeElement.dispatchEvent(enterEvent);
button.nativeElement.dispatchEvent(spaceEvent);
expect(keydownHandled).toBeFalse();
});
it('should allow click and keydown events when not disabled', () => {
let clickHandled = false;
let keydownHandled = false;
button.nativeElement.addEventListener('click', () => clickHandled = true);
button.nativeElement.addEventListener('keydown', () => keydownHandled = true);
button.nativeElement.click();
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
button.nativeElement.dispatchEvent(enterEvent);
button.nativeElement.dispatchEvent(spaceEvent);
expect(clickHandled).toBeTrue();
expect(keydownHandled).toBeTrue();
});
});

View File

@@ -13,7 +13,7 @@
class="btn btn-outline-primary selection" class="btn btn-outline-primary selection"
(blur)="close.emit($event)" (blur)="close.emit($event)"
(click)="close.emit($event)" (click)="close.emit($event)"
[disabled]="disabled" [dsBtnDisabled]="disabled"
ngbDropdownToggle> ngbDropdownToggle>
<ng-content select=".selection"></ng-content> <ng-content select=".selection"></ng-content>
</button> </button>

View File

@@ -8,6 +8,8 @@ import {
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../btn-disabled.directive';
/** /**
* Component which represent a DSpace dropdown selector. * Component which represent a DSpace dropdown selector.
*/ */
@@ -16,7 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
templateUrl: './ds-select.component.html', templateUrl: './ds-select.component.html',
styleUrls: ['./ds-select.component.scss'], styleUrls: ['./ds-select.component.scss'],
standalone: true, standalone: true,
imports: [NgbDropdownModule, NgIf, TranslateModule], imports: [NgbDropdownModule, NgIf, TranslateModule, BtnDisabledDirective],
}) })
export class DsSelectComponent { export class DsSelectComponent {

Some files were not shown because too many files have changed in this diff Show More