Skip to content

Commit f871496

Browse files
authored
fix(popover): NO-JIRA don't hide dialog from assistive tech while it still holds focus on close (#1392)
1 parent 725fd81 commit f871496

6 files changed

Lines changed: 31 additions & 1 deletion

File tree

1 Byte
Loading
4 Bytes
Loading
0 Bytes
Loading
3 Bytes
Loading

packages/dialtone-vue/components/Popover/Popover.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,29 @@ describe('DtPopover Tests', () => {
373373

374374
expect(document.activeElement).toBe(last);
375375
});
376+
377+
it('does not mark the dialog aria-hidden merely because isOpen became false', () => {
378+
// closePopover() flips isOpen synchronously; per the fix, that alone must not hide
379+
// the dialog from assistive technology, since a descendant may still hold focus and
380+
// the leave transition hasn't restored it yet. Only onLeaveTransitionComplete may do
381+
// that, once focus has actually moved off the dialog.
382+
expect(wrapper.vm.isDialogAriaHidden).toBe(false);
383+
384+
wrapper.vm.closePopover();
385+
386+
expect(wrapper.vm.isDialogAriaHidden).toBe(false);
387+
});
388+
389+
it('marks the dialog aria-hidden only after onLeaveTransitionComplete restores focus', async () => {
390+
const last = popoverWindow.find('[data-qa="trap-last"]').element;
391+
last.focus();
392+
393+
wrapper.vm.closePopover();
394+
await wrapper.vm.onLeaveTransitionComplete();
395+
396+
expect(document.activeElement).toBe(button.element);
397+
expect(wrapper.vm.isDialogAriaHidden).toBe(true);
398+
});
376399
});
377400

378401
describe('When not modal', () => {

packages/dialtone-vue/components/Popover/Popover.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
}"
5555
:role="role"
5656
:data-qa="$attrs['data-qa'] ? `${$attrs['data-qa']}__dialog` : 'dt-popover'"
57-
:aria-hidden="`${!isOpen}`"
57+
:aria-hidden="`${isDialogAriaHidden}`"
5858
:aria-labelledby="labelledBy"
5959
:aria-label="ariaLabel"
6060
:aria-modal="`${!modal}`"
@@ -640,6 +640,9 @@ export default {
640640
mutationObserver: null,
641641
isOutsideViewport: false,
642642
isOpen: false,
643+
// Only true once the leave transition (and any focus restoration in onLeaveTransitionComplete) has
644+
// finished, so aria-hidden is never applied while the dialog still holds focus or is still visible.
645+
isDialogAriaHidden: true,
643646
toAppear: false,
644647
anchorEl: null,
645648
popoverContentEl: null,
@@ -754,6 +757,7 @@ export default {
754757
755758
isOpen (isOpen, isPrev) {
756759
if (isOpen) {
760+
this.isDialogAriaHidden = false;
757761
this.initTippyInstance();
758762
this.tip?.show();
759763
} else if (!isOpen && isPrev !== isOpen) {
@@ -995,6 +999,9 @@ export default {
995999
this.enableScrolling();
9961000
}
9971001
if (this._isUnmounting) return;
1002+
// Focus has been moved off the dialog (or was never trapped in it) by this point, so it's now
1003+
// safe to hide it from assistive technology without triggering a focused-descendant violation.
1004+
this.isDialogAriaHidden = true;
9981005
this.tip?.unmount();
9991006
this.$emit('opened', false);
10001007
if (this.open !== null) {

0 commit comments

Comments
 (0)