From 6f4cd94832014cb2cd7dbbfca8c92bb7b266ed87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20D=C3=A9ramond?= Date: Tue, 30 Apr 2024 12:45:59 +0200 Subject: [PATCH] Keep `role="dialog"` after closing modal/offcanvas if already in the markup --- js/src/modal.js | 14 ++++++++++++-- js/src/offcanvas.js | 13 +++++++++++-- js/tests/unit/modal.spec.js | 29 +++++++++++++++++++++++++++++ js/tests/unit/offcanvas.spec.js | 20 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/js/src/modal.js b/js/src/modal.js index dd61649ecc..01a3d7f75c 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -67,6 +67,7 @@ class Modal extends BaseComponent { constructor(element, config) { super(element, config) + this._deleteDialogRoleWhenHiding = false this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element) this._backdrop = this._initializeBackDrop() this._focustrap = this._initializeFocusTrap() @@ -177,7 +178,12 @@ class Modal extends BaseComponent { this._element.style.display = 'block' this._element.removeAttribute('aria-hidden') this._element.setAttribute('aria-modal', true) - this._element.setAttribute('role', 'dialog') + + if (this._element.getAttribute('role') !== 'dialog') { + this._deleteDialogRoleWhenHiding = true + this._element.setAttribute('role', 'dialog') + } + this._element.scrollTop = 0 const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog) @@ -246,7 +252,11 @@ class Modal extends BaseComponent { this._element.style.display = 'none' this._element.setAttribute('aria-hidden', true) this._element.removeAttribute('aria-modal') - this._element.removeAttribute('role') + + if (this._deleteDialogRoleWhenHiding) { + this._element.removeAttribute('role') + } + this._isTransitioning = false this._backdrop.hide(() => { diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 8d1feb13bb..8e82f233b8 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -66,6 +66,7 @@ class Offcanvas extends BaseComponent { constructor(element, config) { super(element, config) + this._deleteDialogRoleWhenHiding = false this._isShown = false this._backdrop = this._initializeBackDrop() this._focustrap = this._initializeFocusTrap() @@ -109,7 +110,12 @@ class Offcanvas extends BaseComponent { } this._element.setAttribute('aria-modal', true) - this._element.setAttribute('role', 'dialog') + + if (this._element.getAttribute('role') !== 'dialog') { + this._deleteDialogRoleWhenHiding = true + this._element.setAttribute('role', 'dialog') + } + this._element.classList.add(CLASS_NAME_SHOWING) const completeCallBack = () => { @@ -145,7 +151,10 @@ class Offcanvas extends BaseComponent { const completeCallback = () => { this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING) this._element.removeAttribute('aria-modal') - this._element.removeAttribute('role') + + if (this._deleteDialogRoleWhenHiding) { + this._element.removeAttribute('role') + } if (!this._config.scroll) { new ScrollBarHelper().reset() diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 2aa0b7655c..429fc15000 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -709,6 +709,35 @@ describe('Modal', () => { }) }) + it('should hide a modal and not remove role="dialog" if already present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) + + modalEl.addEventListener('hide.bs.modal', event => { + expect(event).toBeDefined() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(backdropSpy).toHaveBeenCalled() + resolve() + }) + + modal.show() + }) + }) + it('should close modal when clicking outside of modal-content', () => { return new Promise(resolve => { fixtureEl.innerHTML = '' diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 3b6c98c100..b455de62d6 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -551,6 +551,26 @@ describe('Offcanvas', () => { }) }) + it('should hide a shown element and not remove role="dialog" if already present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(offCanvasEl.getAttribute('role')).toEqual('dialog') + expect(spy).toHaveBeenCalled() + resolve() + }) + + offCanvas.hide() + }) + }) + it('should not fire hidden when hide is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '
'