mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[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:
committed by
GitHub
parent
13e6a9436f
commit
974b84d580
@@ -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>
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
+8
-1
@@ -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>
|
||||
}
|
||||
|
||||
+1
@@ -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"
|
||||
|
||||
+4
-1
@@ -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>
|
||||
|
||||
+6
-3
@@ -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>
|
||||
}
|
||||
|
||||
+10
-2
@@ -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] : ''"
|
||||
|
||||
+6
@@ -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="#"
|
||||
|
||||
+9
-6
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
+10
-6
@@ -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]" />
|
||||
|
||||
+1
@@ -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 }"
|
||||
|
||||
+4
-3
@@ -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"
|
||||
|
||||
+7
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user