[GIT] Evergreen ILS branch rel_3_15 updated. 0af85e02e4d30304d9aa3d528e49461f5fe7b795

This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "Evergreen ILS". The branch, rel_3_15 has been updated via 0af85e02e4d30304d9aa3d528e49461f5fe7b795 (commit) from 33f1e17c1450142a6a1ffe94772cb39e5d7e5387 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 0af85e02e4d30304d9aa3d528e49461f5fe7b795 Author: Stephanie Leary <stephanie.leary@equinoxoli.org> Date: Wed Mar 19 22:44:02 2025 +0000 LP1947868 Dialog: focus on first focusable element On dialog open, places the cursor on the first focusable element in .modal-body. This may be any interactive element: links, inputs, selects, details summaries, etc. If none, focus will land on the first footer button, or as a last resort, the [X] close button in the dialog header. In alert dialogs, focus is placed on the modal body as a whole rather than the OK button when there is nothing interactive in the content. This avoids the issue where barcode scanners' automatically added 'Enter' can accidentally dismiss the alert telling the user that something went wrong. Release-note: Sets focus on the first interactive element in dialog body Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> Signed-off by: Ruth Frasur Davis <redavis4974@gmail.com> Signed-off by: Shula Link <slink@gchrl.org> Signed-off by: Gina Monti <gmonti@biblio.org> Signed-off by: Dan Guarracino <dguarracino@owwl.org> Signed-off by: Michele Morgan <mmorgan@noblenet.org> Signed-off by: Terran McCanna <tmccanna@georgialibraries.org> diff --git a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html index 87a96637a7..722248c3d1 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html +++ b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.html @@ -1,9 +1,9 @@ <ng-template #dialogContent> - <div class="modal-body"> - <h4 class="modal-title"> - <span *ngIf="!dialogTitle" i18n>Could Not Complete The Action</span> + <div class="modal-body modal-alert" tabindex="-1" role="status"> + <h2 class="modal-title text-reset mb-3"> + <span *ngIf="!dialogTitle" i18n>Could not complete the action</span> <span *ngIf="dialogTitle">{{dialogTitle}}</span> - </h4> + </h2> <ng-container *ngIf="!dialogBodyTemplate"> <div class="alert" [ngClass]="'alert-' + alertType">{{dialogBody}}</div> </ng-container> @@ -13,7 +13,7 @@ <ng-content><!-- Place for projected content; for example: <eg-alert-dialog><ul><li>render me too!</li></ul></eg-alert-dialog> --></ng-content> </div> <div class="modal-footer"> - <button type="button" class="btn btn-success" + <button type="button" class="btn btn-normal" (click)="close()" i18n>OK</button> </div> </ng-template> diff --git a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts index 9911cf0195..b8183c074e 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts +++ b/Open-ILS/src/eg2/src/app/share/dialog/alert.component.ts @@ -3,7 +3,8 @@ import {DialogComponent} from '@eg/share/dialog/dialog.component'; @Component({ selector: 'eg-alert-dialog', - templateUrl: './alert.component.html' + templateUrl: './alert.component.html', + styles: ['.modal-alert.modal-body:is(:focus, :focus-visible) { outline: 0.25rem solid var(--bs-border-color-translucent); }'] }) /** diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts index b4a37be97c..668b23b56f 100644 --- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts +++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts @@ -63,8 +63,6 @@ export class DialogComponent implements OnInit { // The modalRef allows direct control of the modal instance. protected modalRef: NgbModalRef = null; - public focusable: string; - constructor(private modalService: NgbModal) {} // Close all active dialogs @@ -79,26 +77,6 @@ export class DialogComponent implements OnInit { ngOnInit() { this.onOpen$ = new EventEmitter<any>(); - - const notFocusable = ':is(:disabled, [inert], [inert] *, [hidden], [hidden] *, [tabindex^="-"])'; - const isFocusable = [ - '[egAutofocus]', - '[ngbAutofocus]', - 'a[href]', - 'area[href]', - 'input:not([type="hidden"]):not(fieldset:disabled *)', - 'select:not(fieldset:disabled *)', - 'textarea:not(fieldset:disabled *)', - 'details > summary:first-of-type:not(details:not([open]) > details summary)', - 'details:not(:has(> summary)):not(details:not([open]) > details)', - 'button', - 'iframe', - 'audio[controls]', - 'video[controls]', - '[contenteditable]', - '[tabindex]' - ].join(', '); - this.focusable = `:is(${isFocusable}):not(${notFocusable})`; } open(options: NgbModalOptions = { backdrop: 'static' }): Observable<any> { @@ -121,7 +99,7 @@ export class DialogComponent implements OnInit { setTimeout(() => { this.onOpen$.emit(true); this._setFocus(); - }); + }, 100); } return new Observable(observer => { @@ -141,13 +119,26 @@ export class DialogComponent implements OnInit { } // Look for the first focusable element in .modal-body. - // If none, focus will default to the 'X' close button, if present, or the first footer button + // Fallbacks are the footer buttons, then (implicitly) the 'X' close button in the dialog header private _setFocus() { if (!this.modalRef) {return;} if (!this._elRef.nativeElement.contains(this._document.activeElement)) { - const elementToFocus = this._elRef.nativeElement.querySelector('.modal-body ' + this.focusable) as HTMLElement; - // console.debug('elementToFocus', elementToFocus); - setTimeout(() => elementToFocus?.focus()); + const dialogEl = this.modalRef['_windowCmptRef'].instance['_elRef'].nativeElement; + const dialogBodySelector = `.modal-body ${this.getFocusable()}`; + const dialogFooterSelector = `.modal-footer ${this.getFocusable()}`; + setTimeout(() => { + let elementToFocus = dialogEl.querySelector(dialogBodySelector) as HTMLElement; + if (!elementToFocus) { + // if this is an alert dialog, focus the whole body rather than a footer button + if (this.hasOwnProperty('alertType')) { // eslint-disable-line no-prototype-builtins + elementToFocus = dialogEl.querySelector('.modal-body') as HTMLElement; + } else { + elementToFocus = dialogEl.querySelector(dialogFooterSelector) as HTMLElement; + } + } + elementToFocus?.focus(); + console.debug('elementToFocus', elementToFocus); + }); } } @@ -155,6 +146,28 @@ export class DialogComponent implements OnInit { setTimeout(() => this.returnFocusTo.focus()); } + public getFocusable() { + const notFocusable = ':is(:disabled, [inert], [inert] *, [hidden], [hidden] *, [tabindex^="-"])'; + const isFocusable = [ + '[egAutofocus]', + '[ngbAutofocus]', + 'a[href]', + 'area[href]', + 'input:not([type="hidden"]):not(fieldset:disabled *)', + 'select:not(fieldset:disabled *)', + 'textarea:not(fieldset:disabled *)', + 'details > summary:first-of-type:not(details:not([open]) > details summary)', + 'details:not(:has(> summary)):not(details:not([open]) > details)', + 'button', + 'iframe', + 'audio[controls]', + 'video[controls]', + '[contenteditable]', + '[tabindex]' + ].join(', '); + return `:is(${isFocusable}):not(${notFocusable})`; + } + // Send a response to the caller without closing the dialog. respond(value: any) { if (this.observer && value !== undefined) { ----------------------------------------------------------------------- Summary of changes: .../eg2/src/app/share/dialog/alert.component.html | 10 ++-- .../eg2/src/app/share/dialog/alert.component.ts | 3 +- .../eg2/src/app/share/dialog/dialog.component.ts | 67 +++++++++++++--------- 3 files changed, 47 insertions(+), 33 deletions(-) hooks/post-receive -- Evergreen ILS
participants (1)
-
Git User