[67399] Fix accessibility issues in Angular templates detected by ESLint (#21339)

* Tell eslint to ignore click rule on router links

See https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/click-events-have-key-events.md

* Pass fixing accessibility issues found by ESLint

- fixes failing tests in configuration modal
- uses button instead of link in my account timer
- uses button instead of div for buttons of scrollable tabs
- sets correct role for links and divs which are clickable
- sets href on links

* Add role="button" and preventDefault() to accessibility fixes

Co-authored-by: myabc <755+myabc@users.noreply.github.com>

* remove tabindex for the item of draggable auto completer

* remove prevent default from drop modal

* Fix test to select the button

---------

Co-authored-by: Alexander Brandon Coles <a.coles@openproject.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: myabc <755+myabc@users.noreply.github.com>
This commit is contained in:
Behrokh Satarnejad
2026-02-10 14:37:32 +01:00
committed by GitHub
parent 13e6a9436f
commit 974b84d580
23 changed files with 110 additions and 45 deletions
+4
View File
@@ -146,6 +146,10 @@ export default defineConfig([
...angular.configs.templateAccessibility,
],
rules: {
'@angular-eslint/template/click-events-have-key-events': [
'error',
{ 'ignoreWithDirectives': ['uiSref'] }
],
'@angular-eslint/template/elements-content': [
'error',
{ 'allowList': ['textContent'] }
@@ -66,14 +66,18 @@
<ng-template op-autocompleter-option-tmp let-item let-index="index" let-search="searchTerm">
@if (!item.id) {
<div>
<div tabindex="-1" class="global-search--option" (click)="followItem(item)">
<a tabindex="0"
href="#"
class="global-search--option"
(click)="$event.preventDefault(); followItem(item)"
(keydown.enter)="$event.preventDefault(); followItem(item)"
(keydown.space)="$event.preventDefault(); followItem(item)">
<div class="global-search--search-term">{{ currentValue }}</div>
<div class="global-search--project-scope" title="{{ item.projectScope }}">{{ item.text }} ↵</div>
</div>
</a>
</div>
} @else {
<a
tabindex="-1"
class="global-search--option"
[href]="wpPath(item.id)"
(click)="redirectToWp(item.id, $event)"
@@ -1,8 +1,12 @@
@if (!editing) {
<div
(click)="startEditing()"
role="button"
tabindex="0"
class="group-edit-handler"
[textContent]="name"
class="group-edit-handler">
(click)="startEditing()"
(keydown.enter)="startEditing()"
(keydown.space)="startEditing()">
</div>
}
@if (editing) {
@@ -15,7 +15,12 @@
/>
</div>
<div class="type-form-query">
<span class="type-form-query-group--edit-button" (click)="editQuery.emit()">
<span class="type-form-query-group--edit-button"
role="button"
tabindex="0"
(click)="editQuery.emit()"
(keydown.enter)="editQuery.emit()"
(keydown.space)="editQuery.emit()">
<op-icon icon-classes="button--icon icon-edit" />
{{ text.edit_query }}
</span>
@@ -17,13 +17,14 @@
</div>
}
@if (viewerVisible && createAllowed) {
<a
[title]="text.add_viewpoint"
<button
role="button"
class="button"
[title]="text.add_viewpoint"
(click)="saveViewpoint(workPackage)">
<op-icon icon-classes="button--icon icon-add" />
<span class="button--text"> {{text.viewpoint}} </span>
</a>
</button>
}
</div>
}
@@ -90,7 +90,7 @@
<div #inspectorPane
class="op-ifc-viewer--inspector-container"
[class.op-ifc-viewer--inspector-container-hidden]="!(inspectorVisible$ | async)"
[class.op-ifc-viewer--inspector-container-hidden]="(inspectorVisible$ | async) === false"
data-test-selector="op-ifc-viewer--inspector-container">
</div>
</div>
@@ -41,7 +41,11 @@
}
@if (board.editable) {
<div class="boards-list--add-item -no-text-select"
(click)="addList(board)">
role="button"
tabindex="0"
(click)="addList(board)"
(keydown.enter)="addList(board)"
(keydown.space)="addList(board)">
<div class="boards-list--add-item-text">
<op-icon icon-classes="icon-add icon-context" />
<span [textContent]="text.addList"></span>
@@ -1,6 +1,10 @@
<div
class="op-ian-item--row"
tabindex="0"
role="button"
(click)="onClick()"
(keydown.enter)="$event.preventDefault(); onClick()"
(keydown.space)="$event.preventDefault(); onClick()"
(dblclick)="onDoubleClick()"
>
@if (workPackage$ && (workPackage$ | async); as workPackage) {
@@ -51,9 +55,12 @@
<i
data-test-selector="mark-as-read-button"
class="op-ian-item--button icon-mark-read"
tabindex="0"
role="button"
[title]="text.mark_as_read"
(click)="markAsRead($event, aggregatedNotifications)"
tabindex="0"
(keydown.enter)="$event.preventDefault(); markAsRead($event, aggregatedNotifications)"
(keydown.space)="$event.preventDefault(); markAsRead($event, aggregatedNotifications)"
>
</i>
}
@@ -132,6 +132,7 @@
}
<div class="op-team-planner--footer" data-test-selector="op-team-planner-footer">
<!-- eslint-disable-next-line @angular-eslint/template/no-negated-async -->
@if (!(showAddAssignee$ | async) && !(dropzone$ | async)?.dragging) {
<div
class="op-team-planner--add-assignee"
@@ -1,7 +1,10 @@
<span
class="wp-replacement-label"
(click)="activate($event)"
tabindex="-1"
(keydown.space)="$event.preventDefault(); activate($event)"
(keydown.enter)="$event.preventDefault(); activate($event)"
tabindex="0"
role="button"
attr.data-test-selector="{{fieldName}}">
<ng-content />
</span>
@@ -28,7 +28,7 @@
<div class="FormControl-spacingWrapper">
<div class="FormControl width-full">
<span class="FormControl-label" [textContent]="dragAreaLabel"></span>
<span class="FormControl-label" [textContent]="dragAreaLabel" [tabIndex]="0" role="region"></span>
<div
class="op-draggable-autocomplete--selected"
@@ -42,10 +42,13 @@
[textContent]="item.name"
></span>
@if (isRemovable(item)) {
<a
<div
tabindex="0"
(click)="remove(item)"
(keydown.enter)="remove(item)"
(keydown.space)="remove(item)"
class="op-draggable-autocomplete--remove-item icon-remove"
></a>
></div>
}
</div>
}
@@ -222,7 +222,11 @@
<ng-template let-item let-search="search" let-clear="clear" #defaultLabel>
@if (resource === 'work_packages') {
<span class="ng-value-icon left" (click)="clear(item)">×</span>
<span class="ng-value-icon left"
tabindex="0"
(click)="clear(item)"
(keydown.enter)="clear(item)"
(keydown.space)="clear(item)">×</span>
@if (item.id) {
<span
class="ng-value-label"
@@ -234,7 +238,11 @@
}
@if (resource !== 'work_packages') {
<span class="ng-value-icon left" (click)="clear(item)">×</span>
<span class="ng-value-icon left"
tabindex="0"
(click)="clear(item)"
(keydown.enter)="$event.preventDefault(); clear(item)"
(keydown.space)="$event.preventDefault(); clear(item)">×</span>
<span
[ngOptionHighlight]="search"
[ngClass]=" additionalClassProperty ? item[additionalClassProperty] : ''"
@@ -4,7 +4,10 @@
<ul class="op-tab-row">
<li
class="op-tab-row--tab"
tabindex="0"
(click)="autocompleter.changeMode('all')"
(keydown.enter)="$event.preventDefault(); autocompleter.changeMode('all')"
(keydown.space)="$event.preventDefault(); autocompleter.changeMode('all')"
>
<a
href="#"
@@ -16,7 +19,10 @@
</li>
<li
class="op-tab-row--tab"
tabindex="0"
(click)="autocompleter.changeMode('recent')"
(keydown.space)="$event.preventDefault(); autocompleter.changeMode('recent')"
(keydown.enter)="$event.preventDefault(); autocompleter.changeMode('recent')"
>
<a
href="#"
@@ -3,13 +3,16 @@
(closed)="opened = false"
class="op-exclusion-info"
>
<svg
slot="trigger"
info-icon
class="button--icon op-exclusion-info--icon"
size="small"
<span
tabindex="0"
role="button"
class="op-exclusion-info--icon"
(click)="toggleOpen($event)"
></svg>
(keydown.enter)="toggleOpen($event)"
(keydown.space)="toggleOpen($event)"
>
<svg info-icon size="small" aria-hidden="true"></svg>
</span>
<ng-container slot="body" class="op-exclusion-info--modal">
<div class="op-exclusion-info--modal">
@@ -40,14 +40,11 @@ export class DynamicIconDirective {
readonly icon = input.required<string>();
readonly size = input<SVGSize>('medium');
private loaded = false;
constructor() {
effect(() => {
const name = this.icon();
if (!name || this.loaded) return;
if (!name) return;
this.loaded = true;
this.renderIcon(name);
});
}
@@ -75,20 +75,24 @@
</li>
}
</ul>
<div
#scrollLeftBtn [hidden]="hideLeftButton"
<button
#scrollLeftBtn
type="button"
class="op-scrollable-tabs--button op-scrollable-tabs--button_left"
[hidden]="hideLeftButton"
(click)="scrollLeft()"
>
<span class="icon-arrow-left2"></span>
</div>
<div
#scrollRightBtn [hidden]="hideRightButton"
</button>
<button
#scrollRightBtn
type="button"
class="op-scrollable-tabs--button op-scrollable-tabs--button_right"
[hidden]="hideRightButton"
(click)="scrollRight()"
>
<span class="icon-arrow-right2"></span>
</div>
</button>
</div>
<ul class="op-scrollable-tabs--actions">
<ng-content select="[slot=actions]" />
@@ -16,6 +16,7 @@
<p>
<strong>{{ text.tracking_time }}: {{ elapsed$ | async }}</strong>
<br/>
<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
<a
uiSref="work-packages.show"
[uiParams]="{ workPackageId: active.entity.id }"
@@ -1,13 +1,14 @@
@if (timer$ | async; as timer) {
<span class="op-timer-account-menu--timer"> {{text.tracking}}: {{ elapsed$ | async }}
<a
<button
type="button"
class="op-timer-account-menu--timer-icon"
data-test-selector="op-timer-account-menu-stop"
(click)="stopTimer()"
>
>
<svg op-stopwatch-stop-icon size="small" />
<span>{{text.stop}}</span>
</a>
</button>
</span>
<a
class="op-timer-account-menu--wp-details"
@@ -12,8 +12,14 @@
line-height: $spot-spacing-2
&--timer-icon
margin-left: auto
cursor: pointer
border: none
background: none
align-items: center
padding: 0
display: inline-flex
margin-left: 0.5rem
color: var(--accent-color)
.octicon
margin-right: var(--base-size-4)
@@ -15,9 +15,10 @@
<span [textContent]="toast.message"></span>
<span [textContent]="' '"></span>
@if (toast.link) {
<a class="op-toast--target-link"
(click)="executeTarget()"
[textContent]="toast.link!.text">
<a href="#"
class="op-toast--target-link"
[textContent]="toast.link!.text"
(click)="executeTarget()">
</a>
}
</p>
@@ -28,11 +29,10 @@
<div>
@if (canBeHidden) {
<div>
<a (click)="show = true" [hidden]="show">
<a href="#" [hidden]="show" (click)="show = true">
<svg chevron-right-icon size="small"></svg>
</a>
<a (click)="show = false" [hidden]="!show">
<a href="#" [hidden]="!show" (click)="show = false">
<svg chevron-down-icon size="small"></svg>
</a>
<span [textContent]="uploadText"></span>
@@ -13,6 +13,8 @@
[ngClass]="['spot-drop-modal--body', 'spot-container' ]"
[class.spot-drop-modal--body_not-full-screen]="notFullscreen"
(click)="onBodyClick($event)"
(keydown.enter)="onBodyClick($event)"
(keydown.space)="onBodyClick($event)"
cdkTrapFocus
tabindex="0"
>
@@ -25,6 +25,7 @@
&--button
display: block
border: none
width: 20px
position: absolute
top: 0px
@@ -59,7 +59,7 @@ module Components
end
def add_viewpoint
page.find("a.button", text: "Viewpoint").click
click_button "Viewpoint"
end
end
end