{{ 'access-control-cancel' | translate }}
{{ 'access-control-execute' | translate }}
diff --git a/src/app/shared/btn-disabled.directive.ts b/src/app/shared/btn-disabled.directive.ts
new file mode 100644
index 00000000000..512aa87edef
--- /dev/null
+++ b/src/app/shared/btn-disabled.directive.ts
@@ -0,0 +1,57 @@
+import { Directive, Input, HostBinding, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[dsBtnDisabled]'
+})
+
+/**
+ * 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();
+ }
+ }
+}
+
diff --git a/src/app/shared/disabled-directive.spec.ts b/src/app/shared/disabled-directive.spec.ts
new file mode 100644
index 00000000000..71515c48ff3
--- /dev/null
+++ b/src/app/shared/disabled-directive.spec.ts
@@ -0,0 +1,97 @@
+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: `
+ Test Button
+ `
+})
+class TestComponent {
+ isDisabled = false;
+}
+
+describe('DisabledDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ let button: DebugElement;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent, BtnDisabledDirective]
+ });
+ 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();
+ });
+});
diff --git a/src/app/shared/ds-select/ds-select.component.html b/src/app/shared/ds-select/ds-select.component.html
index c12ea613472..e87b7c5cc6c 100644
--- a/src/app/shared/ds-select/ds-select.component.html
+++ b/src/app/shared/ds-select/ds-select.component.html
@@ -13,7 +13,7 @@
class="btn btn-outline-primary selection"
(blur)="close.emit($event)"
(click)="close.emit($event)"
- [disabled]="disabled"
+ [dsBtnDisabled]="disabled"
ngbDropdownToggle>
-
\ No newline at end of file
+
diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss
index 99cc075dbe7..07c1cf538e0 100644
--- a/src/styles/_global-styles.scss
+++ b/src/styles/_global-styles.scss
@@ -496,3 +496,14 @@ ngb-accordion {
// We use underline to discern link from text as we can't make color lighter on a white bg
text-decoration: underline;
}
+
+// An element that is disabled should not have focus styles to avoid confusion
+// It however is still focusable for screen readers and keyboard navigation
+.disabled {
+ pointer-events: none;
+}
+
+.disabled:focus {
+ outline: none;
+ box-shadow: none;
+}