Merge pull request #4335 from alexandrevryghem/w2p-130479_fix-accessibility-issues-for-the-language-toggle-and-recent-submissions_contribute-main

Improve accessibility for screen readers
This commit is contained in:
Tim Donohue
2025-05-14 16:41:03 -05:00
committed by GitHub
36 changed files with 155 additions and 109 deletions

View File

@@ -15,24 +15,24 @@ describe('Header', () => {
cy.visit('/'); cy.visit('/');
// Click the language switcher (globe) in header // Click the language switcher (globe) in header
cy.get('a[data-test="lang-switch"]').click(); cy.get('button[data-test="lang-switch"]').click();
// Click on the "Deusch" language in dropdown // Click on the "Deusch" language in dropdown
cy.get('#language-menu-list li').contains('Deutsch').click(); cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
// HTML "lang" attribute should switch to "de" // HTML "lang" attribute should switch to "de"
cy.get('html').invoke('attr', 'lang').should('eq', 'de'); cy.get('html').invoke('attr', 'lang').should('eq', 'de');
// Login menu should now be in German // Login menu should now be in German
cy.get('a[data-test="login-menu"]').contains('Anmelden'); cy.get('[data-test="login-menu"]').contains('Anmelden');
// Change back to English from language switcher // Change back to English from language switcher
cy.get('a[data-test="lang-switch"]').click(); cy.get('button[data-test="lang-switch"]').click();
cy.get('#language-menu-list li').contains('English').click(); cy.get('#language-menu-list div[role="option"]').contains('English').click();
// HTML "lang" attribute should switch to "en" // HTML "lang" attribute should switch to "en"
cy.get('html').invoke('attr', 'lang').should('eq', 'en'); cy.get('html').invoke('attr', 'lang').should('eq', 'en');
// Login menu should now be in English // Login menu should now be in English
cy.get('a[data-test="login-menu"]').contains('Log In'); cy.get('[data-test="login-menu"]').contains('Log In');
}); });
}); });

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -3,7 +3,7 @@
<div class="col-3 col-md-2"> <div class="col-3 col-md-2">
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'" [defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'" [alt]="'thumbnail.orgunit.alt'"

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'" [defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'" [alt]="'thumbnail.person.alt'"

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0"> [routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/project-placeholder.svg'" [defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'" [alt]="'thumbnail.project.alt'"

View File

@@ -5,12 +5,14 @@
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/> <img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a> </a>
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0"> <div class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
<ds-search-navbar></ds-search-navbar> <ds-search-navbar></ds-search-navbar>
<div role="toolbar" [attr.aria-label]="'nav.user.description' | translate">
<ds-lang-switch></ds-lang-switch> <ds-lang-switch></ds-lang-switch>
<ds-context-help-toggle></ds-context-help-toggle> <ds-context-help-toggle></ds-context-help-toggle>
<ds-auth-nav-menu></ds-auth-nav-menu> <ds-auth-nav-menu></ds-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar> <ds-impersonate-navbar></ds-impersonate-navbar>
</div>
@if (isMobile$ | async) { @if (isMobile$ | async) {
<div class="ps-2"> <div class="ps-2">
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()" <button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
@@ -20,7 +22,7 @@
</button> </button>
</div> </div>
} }
</nav> </div>
</div> </div>
</div> </div>
</header> </header>

View File

@@ -23,7 +23,7 @@
} }
} }
.navbar { .navbar, div[role="toolbar"] {
display: flex; display: flex;
gap: calc(var(--bs-spacer) / 3); gap: calc(var(--bs-spacer) / 3);
align-items: center; align-items: center;

View File

@@ -3,13 +3,17 @@
<div class="mt-4" [ngClass]="placeholderFontClass" @fadeIn> <div class="mt-4" [ngClass]="placeholderFontClass" @fadeIn>
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div> <div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
<h2> {{ 'home.recent-submissions.head' | translate }}</h2> <h2> {{ 'home.recent-submissions.head' | translate }}</h2>
<ul class="list-unstyled m-0 p-0">
@for (item of itemRD?.payload?.page; track item) { @for (item of itemRD?.payload?.page; track item) {
<div class="my-4"> <li class="my-4">
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4"> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
</ds-listable-object-component-loader> </ds-listable-object-component-loader>
</div> </li>
} }
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start ng-tns-c290-40" role="button" tabindex="0"> {{'vocabulary-treeview.load-more' | translate }} ...</button> </ul>
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start" role="link" tabindex="0">
{{ 'vocabulary-treeview.load-more' | translate }} ...
</button>
</div> </div>
} }
@if (itemRD?.hasFailed) { @if (itemRD?.hasFailed) {

View File

@@ -9,7 +9,7 @@
@if ((isMobile$ | async) && (isAuthenticated$ | async)) { @if ((isMobile$ | async) && (isAuthenticated$ | async)) {
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu> <ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
} }
<div class="navbar-nav align-items-md-center me-auto shadow-none gapx-3"> <div class="navbar-nav align-items-md-center me-auto shadow-none gapx-3" role="menubar">
@for (section of (sections | async); track section) { @for (section of (sections | async); track section) {
<ng-container <ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container> *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>

View File

@@ -1,20 +1,25 @@
@let isAuthenticated = (isAuthenticated$ | async);
@if ((isMobile$ | async) !== true) { @if ((isMobile$ | async) !== true) {
<div class="navbar-nav me-auto" data-test="auth-nav"> <div class="navbar-nav me-auto" data-test="auth-nav">
@if ((isAuthenticated | async) !== true && (showAuth | async)) { @let showAuth = (showAuth$ | async);
@if (isAuthenticated !== true && showAuth) {
<div <div
class="nav-item" class="nav-item"
(click)="$event.stopPropagation();"> (click)="$event.stopPropagation();">
<div ngbDropdown #loginDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown #loginDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate" <button class="dropdownLogin btn btn-link px-0" [attr.aria-label]="'nav.login' |translate"
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
role="menuitem" role="button"
tabindex="0" tabindex="0"
aria-haspopup="menu" aria-haspopup="menu"
aria-controls="loginDropdownMenu" aria-controls="loginDropdownMenu"
[attr.aria-expanded]="loginDrop.isOpen()" [attr.aria-expanded]="loginDrop.isOpen()"
ngbDropdownToggle>{{ 'nav.login' | translate }}</a> ngbDropdownToggle>
{{ 'nav.login' | translate }}
</button>
<div id="loginDropdownMenu" [ngClass]="{'ps-3 pe-3': (loading | async)}" ngbDropdownMenu <div id="loginDropdownMenu" [ngClass]="{'ps-3 pe-3': (loading | async)}" ngbDropdownMenu
role="menu" role="dialog"
aria-modal="true"
[attr.aria-label]="'nav.login' | translate"> [attr.aria-label]="'nav.login' | translate">
<ds-log-in <ds-log-in
[isStandalonePage]="false"></ds-log-in> [isStandalonePage]="false"></ds-log-in>
@@ -22,19 +27,20 @@
</div> </div>
</div> </div>
} }
@if ((isAuthenticated | async) && (showAuth | async)) { @if (isAuthenticated && showAuth) {
<div class="nav-item"> <div class="nav-item">
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" <button
role="menuitem" role="button"
tabindex="0" tabindex="0"
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate" [attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
aria-controls="user-menu-dropdown" aria-controls="user-menu-dropdown"
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
class="dropdownLogout px-1" class="dropdownLogout btn btn-link px-0"
[attr.data-test]="'user-menu' | dsBrowserOnly" [attr.data-test]="'user-menu' | dsBrowserOnly"
ngbDropdownToggle> ngbDropdownToggle>
<i class="fas fa-user-circle fa-lg fa-fw"></i></a> <i class="fas fa-user-circle fa-lg fa-fw"></i>
</button>
<div id="logoutDropdownMenu" ngbDropdownMenu> <div id="logoutDropdownMenu" ngbDropdownMenu>
<ds-user-menu [inExpandableNavbar]="false" (changedRoute)="loggedInDrop.close()"></ds-user-menu> <ds-user-menu [inExpandableNavbar]="false" (changedRoute)="loggedInDrop.close()"></ds-user-menu>
</div> </div>
@@ -44,20 +50,17 @@
</div> </div>
} @else { } @else {
<div data-test="auth-nav"> <div data-test="auth-nav">
@if ((isAuthenticated | async) !== true) { @if (isAuthenticated === true) {
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button" tabindex="0"> <a [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-0" role="link" tabindex="0">
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a>
}
@if ((isAuthenticated | async)) {
<a role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1" role="button" tabindex="0">
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i> <i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
<span class="sr-only">(current)</span> <span class="sr-only">(current)</span>
</a> </a>
} @else {
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0" role="link" tabindex="0">
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a>
} }
</div> </div>
} }
<!-- Do not use ul/li in this menu as it breaks e2e accessibility tests --> <!-- Do not use ul/li in this menu as it breaks e2e accessibility tests -->

View File

@@ -28,3 +28,7 @@
box-shadow: unset; box-shadow: unset;
} }
} }
.dropdown-toggle::after {
margin-left: 0;
}

View File

@@ -64,7 +64,7 @@ export class AuthNavMenuComponent implements OnInit {
* Whether user is authenticated. * Whether user is authenticated.
* @type {Observable<string>} * @type {Observable<string>}
*/ */
public isAuthenticated: Observable<boolean>; public isAuthenticated$: Observable<boolean>;
/** /**
* True if the authentication is loading. * True if the authentication is loading.
@@ -74,7 +74,7 @@ export class AuthNavMenuComponent implements OnInit {
public isMobile$: Observable<boolean>; public isMobile$: Observable<boolean>;
public showAuth = observableOf(false); public showAuth$ = observableOf(false);
public user: Observable<EPerson>; public user: Observable<EPerson>;
@@ -89,14 +89,14 @@ export class AuthNavMenuComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
// set isAuthenticated // set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated)); this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
// set loading // set loading
this.loading = this.store.pipe(select(isAuthenticationLoading)); this.loading = this.store.pipe(select(isAuthenticationLoading));
this.user = this.authService.getAuthenticatedUserFromStore(); this.user = this.authService.getAuthenticatedUserFromStore();
this.showAuth = this.store.pipe( this.showAuth$ = this.store.pipe(
select(routerStateSelector), select(routerStateSelector),
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)), filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
map((router: RouterReducerState) => (!router.state.url.startsWith(LOGIN_ROUTE) map((router: RouterReducerState) => (!router.state.url.startsWith(LOGIN_ROUTE)

View File

@@ -1,25 +1,29 @@
@if (moreThanOneLanguage) { @if (moreThanOneLanguage) {
<div ngbDropdown class="navbar-nav" display="dynamic" placement="bottom-right"> <div ngbDropdown class="navbar-nav" display="dynamic" placement="bottom-right">
<a href="javascript:void(0);" role="menuitem" <button role="button"
[attr.aria-label]="'nav.language' |translate" [attr.aria-label]="'nav.language' |translate"
aria-controls="language-menu-list" aria-controls="language-menu-list"
aria-haspopup="menu" aria-haspopup="menu"
class="dropdown-toggle btn btn-link px-0"
[title]="'nav.language' | translate" [title]="'nav.language' | translate"
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle (click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
data-test="lang-switch" data-test="lang-switch"
tabindex="0"> tabindex="0">
<i class="fas fa-globe-asia fa-lg fa-fw"></i> <i class="fas fa-globe-asia fa-lg fa-fw"></i>
</a> </button>
<ul ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list" role="menu"> <div ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list"
role="listbox">
@for (lang of translate.getLangs(); track lang) { @for (lang of translate.getLangs(); track lang) {
<li class="dropdown-item" tabindex="0" #langSelect <div class="dropdown-item" tabindex="0"
role="menuitem" role="option"
[lang]="lang"
(keyup.enter)="useLang(lang)" (keyup.enter)="useLang(lang)"
(click)="useLang(lang)" (click)="useLang(lang)"
[attr.aria-selected]="lang === translate.currentLang"
[class.active]="lang === translate.currentLang"> [class.active]="lang === translate.currentLang">
{{ langLabel(lang) }} {{ langLabel(lang) }}
</li> </div>
} }
</ul> </div>
</div> </div>
} }

View File

@@ -128,7 +128,7 @@ describe('LangSwitchComponent', () => {
})); }));
it('should define the main A HREF in the UI', (() => { it('should define the main A HREF in the UI', (() => {
expect(langSwitchElement.querySelector('a')).not.toBeNull(); expect(langSwitchElement.querySelector('button.dropdown-toggle')).not.toBeNull();
})); }));
describe('when selecting a language', () => { describe('when selecting a language', () => {

View File

@@ -35,11 +35,11 @@
<div class="mt-2"> <div class="mt-2">
@if (canRegister$ | async) { @if (canRegister$ | async) {
<a class="dropdown-item" [routerLink]="[getRegisterRoute()]" <a class="dropdown-item" [routerLink]="[getRegisterRoute()]"
[attr.data-test]="'register' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.new-user" | translate}}</a> [attr.data-test]="'register' | dsBrowserOnly" tabindex="0">{{"login.form.new-user" | translate}}</a>
} }
@if (canForgot$ | async) { @if (canForgot$ | async) {
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" <a class="dropdown-item" [routerLink]="[getForgotRoute()]"
[attr.data-test]="'forgot' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.forgot-password" | translate}}</a> [attr.data-test]="'forgot' | dsBrowserOnly" tabindex="0">{{"login.form.forgot-password" | translate}}</a>
} }
</div> </div>
} }

View File

@@ -1,5 +1,10 @@
@if (showAccessStatus) { @if (showAccessStatus) {
@if ({ status: accessStatus$ | async, date: embargoDate$ | async }; as accessStatus) { @if ((accessStatus$ | async); as status) {
<span [class]="'badge bg-secondary dont-break-out access-status-list-element-badge ' + accessStatusClass">{{ accessStatus.status | translate: {date: accessStatus.date} }}</span> @let date = embargoDate$ | async;
<span [class]="'badge bg-secondary dont-break-out access-status-list-element-badge ' + accessStatusClass">
<span class="sr-only">{{ 'listelement.badge.access-status' | translate }}</span>
{{ status | translate: { date: date } }}
<span class="sr-only">, </span>
</span>
} }
} }

View File

@@ -19,7 +19,7 @@ import { TruncatePipe } from '../../../../utils/truncate.pipe';
import { AccessStatusObject } from './access-status.model'; import { AccessStatusObject } from './access-status.model';
import { AccessStatusBadgeComponent } from './access-status-badge.component'; import { AccessStatusBadgeComponent } from './access-status-badge.component';
describe('ItemAccessStatusBadgeComponent', () => { describe('AccessStatusBadgeComponent', () => {
let component: AccessStatusBadgeComponent; let component: AccessStatusBadgeComponent;
let fixture: ComponentFixture<AccessStatusBadgeComponent>; let fixture: ComponentFixture<AccessStatusBadgeComponent>;
@@ -100,17 +100,17 @@ describe('ItemAccessStatusBadgeComponent', () => {
function lookForAccessStatusBadgeForItem(status: string) { function lookForAccessStatusBadgeForItem(status: string) {
const badge = fixture.debugElement.query(By.css('span.badge')); const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge.nativeElement.textContent).toEqual(`access-status.${status.toLowerCase()}.listelement.badge`); expect(badge.nativeElement.textContent).toContain(`access-status.${status.toLowerCase()}.listelement.badge`);
} }
function lookForAccessStatusBadgeForBitstream() { function lookForAccessStatusBadgeForBitstream() {
const badge = fixture.debugElement.query(By.css('span.badge')); const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge.nativeElement.textContent).toEqual(`embargo.listelement.badge`); expect(badge.nativeElement.textContent).toContain('embargo.listelement.badge');
} }
function lookForNoAccessStatusBadgeForBitstream() { function lookForNoAccessStatusBadgeForBitstream() {
const badge = fixture.debugElement.query(By.css('span.badge')); const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge.nativeElement.textContent).toEqual(``); expect(badge).toBeNull();
} }
describe('init with item', () => { describe('init with item', () => {

View File

@@ -1,5 +1,7 @@
<div> <div>
<span [className]="badgeClass"> <span [className]="badgeClass">
<span class="sr-only">{{ 'mydspace.status' | translate }}</span>
{{('mydspace.status.' + badgeContent) | translate}} {{('mydspace.status.' + badgeContent) | translate}}
<span class="sr-only">, </span>
</span> </span>
</div> </div>

View File

@@ -1,10 +1,16 @@
@if (privateBadge) { @if (privateBadge) {
<div class="private-badge"> <div class="private-badge">
<span class="badge bg-danger">{{ "item.badge.private" | translate }}</span> <span class="badge bg-danger">
<span class="sr-only">{{ 'item.badge.status' | translate }}</span>
{{ "item.badge.private" | translate }}
<span class="sr-only">, </span>
</span>
</div> </div>
} }
@if (withdrawnBadge) { @if (withdrawnBadge) {
<div class="withdrawn-badge"> <div class="withdrawn-badge">
<span class="sr-only">{{ 'item.badge.status' | translate }}</span>
<span class="badge bg-warning">{{ "item.badge.withdrawn" | translate }}</span> <span class="badge bg-warning">{{ "item.badge.withdrawn" | translate }}</span>
<span class="sr-only">, </span>
</div> </div>
} }

View File

@@ -1,5 +1,7 @@
@if (typeMessage) { @if (typeMessage) {
<span> <span>
<span class="sr-only">{{ 'listelement.badge.dso-type' | translate }}</span>
<span class="badge bg-info">{{ typeMessage | translate }}</span> <span class="badge bg-info">{{ typeMessage | translate }}</span>
<span class="sr-only">, </span>
</span> </span>
} }

View File

@@ -1,6 +1,6 @@
<div class="card"> <div class="card">
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/', dso.id]" class="card-img-top" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -1,6 +1,6 @@
<div class="card"> <div class="card">
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/communities/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/communities/', dso.id]" class="card-img-top" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -4,7 +4,7 @@
</div> </div>
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>

View File

@@ -3,7 +3,7 @@
<div class="col-3 col-md-2"> <div class="col-3 col-md-2">
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out" role="button" tabindex="0"> [routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>

View File

@@ -32,7 +32,7 @@
<ds-loading <ds-loading
message="{{'loading.search-results' | translate}}"></ds-loading> message="{{'loading.search-results' | translate}}"></ds-loading>
} }
@if ((isLoading$ | async) !== true && entriesRD?.payload?.page?.length === 0) { @if ((isLoading$ | async) !== true && entriesRD?.payload?.page?.length === 0 && routeData?.query?.length > 0) {
<div data-test="empty-external-entry-list"> <div data-test="empty-external-entry-list">
<ds-alert [type]="AlertType.Info">{{ 'search.results.empty' | translate }}</ds-alert> <ds-alert [type]="AlertType.Info">{{ 'search.results.empty' | translate }}</ds-alert>
</div> </div>

View File

@@ -503,6 +503,12 @@ describe('SubmissionImportExternalComponent test suite', () => {
if (param === 'entity') { if (param === 'entity') {
return observableOf('Publication'); return observableOf('Publication');
} }
if (param === 'query') {
return observableOf('test');
}
if (param === 'sourceId') {
return observableOf('pubmed');
}
return observableOf({}); return observableOf({});
}); });
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -33,6 +33,8 @@
"error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator",
"listelement.badge.access-status": "Access status:",
"access-status.embargo.listelement.badge": "Embargo", "access-status.embargo.listelement.badge": "Embargo",
"access-status.metadata.only.listelement.badge": "Metadata only", "access-status.metadata.only.listelement.badge": "Metadata only",
@@ -2251,6 +2253,8 @@
"item.edit.authorizations.title": "Edit item's Policies", "item.edit.authorizations.title": "Edit item's Policies",
"item.badge.status": "Item status:",
"item.badge.private": "Non-discoverable", "item.badge.private": "Non-discoverable",
"item.badge.withdrawn": "Withdrawn", "item.badge.withdrawn": "Withdrawn",
@@ -3591,6 +3595,8 @@
"mydspace.show.supervisedWorkspace": "Supervised items", "mydspace.show.supervisedWorkspace": "Supervised items",
"mydspace.status": "My DSpace status:",
"mydspace.status.mydspaceArchived": "Archived", "mydspace.status.mydspaceArchived": "Archived",
"mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validation",
@@ -3655,6 +3661,8 @@
"nav.user.description": "User profile bar", "nav.user.description": "User profile bar",
"listelement.badge.dso-type": "Item type:",
"none.listelement.badge": "Item", "none.listelement.badge": "Item",
"publication-claim.title": "Publication claim", "publication-claim.title": "Publication claim",

View File

@@ -17,7 +17,7 @@
<!-- Search bar and other menus --> <!-- Search bar and other menus -->
<div id="header-right" class="h-100 d-flex flex-row flex-nowrap flex-shrink-0 justify-content-end align-items-center gapx-1 ms-auto"> <div id="header-right" class="h-100 d-flex flex-row flex-nowrap flex-shrink-0 justify-content-end align-items-center gapx-1 ms-auto">
<ds-search-navbar></ds-search-navbar> <ds-search-navbar></ds-search-navbar>
<div role="menubar" class="h-100 d-flex flex-row flex-nowrap align-items-center gapx-1"> <div role="toolbar" class="h-100 d-flex flex-row flex-nowrap align-items-center gapx-1">
<ds-lang-switch></ds-lang-switch> <ds-lang-switch></ds-lang-switch>
<ds-context-help-toggle></ds-context-help-toggle> <ds-context-help-toggle></ds-context-help-toggle>
<ds-impersonate-navbar></ds-impersonate-navbar> <ds-impersonate-navbar></ds-impersonate-navbar>