Skip to content

Commit a5aa3e7

Browse files
fix(image-viewer): DLT-3456 trap focus within the modal via v-dt-focustrap directive (#1313)
1 parent 72b8423 commit a5aa3e7

4 files changed

Lines changed: 26 additions & 12 deletions

File tree

79 Bytes
Loading
-231 Bytes
Loading

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { mount } from '@vue/test-utils';
2+
import { DtFocustrapDirective } from '@/directives/focustrap_directive';
3+
import { flushPromises } from '@/common/utils';
24
import DtImageViewer from './ImageViewer.vue';
35

46
const baseProps = {
@@ -23,10 +25,12 @@ describe('DtImageViewer Tests', () => {
2325
wrapper = mount(DtImageViewer, {
2426
props: { ...baseProps, ...mockProps },
2527
global: {
28+
plugins: [DtFocustrapDirective],
2629
stubs: {
2730
teleport: true,
2831
},
2932
},
33+
attachTo: document.body,
3034
});
3135

3236
imageViewerPreview = wrapper.find('[data-qa="dt-image-viewer-preview"]');
@@ -38,6 +42,7 @@ describe('DtImageViewer Tests', () => {
3842
});
3943

4044
afterEach(() => {
45+
wrapper.unmount();
4146
mockProps = {};
4247
});
4348

@@ -46,7 +51,7 @@ describe('DtImageViewer Tests', () => {
4651

4752
imageViewerFull = wrapper.find('[data-qa="dt-image-viewer-full"]');
4853
fullImage = imageViewerFull.find('img');
49-
closeButton = wrapper.find('[data-qa="dt-image-viewer-close-btn"');
54+
closeButton = wrapper.find('[data-qa="dt-image-viewer-close-btn"]');
5055
overlay = wrapper.find('[data-qa="dt-modal"]');
5156
};
5257

@@ -92,6 +97,23 @@ describe('DtImageViewer Tests', () => {
9297
});
9398
});
9499

100+
describe('Focus trapping', () => {
101+
it('keeps focus trapped inside the open modal (Tab is intercepted)', async () => {
102+
await _openModal();
103+
await flushPromises();
104+
105+
const closeEl = wrapper.find('[data-qa="dt-image-viewer-close-btn"]').element;
106+
closeEl.focus();
107+
expect(document.activeElement).toBe(closeEl);
108+
109+
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true });
110+
overlay.element.dispatchEvent(tabEvent);
111+
112+
// v-dt-focustrap intercepts Tab at the boundary so focus cannot escape the modal.
113+
expect(tabEvent.defaultPrevented).toBe(true);
114+
});
115+
});
116+
95117
describe('Interactivity Tests', () => {
96118
describe('As an image preview', () => {
97119
it('should open on click', async () => {

packages/dialtone-vue/components/ImageViewer/ImageViewer.vue

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
:to="appendTo"
1919
>
2020
<div
21+
v-dt-focustrap="{ active: isOpen, initialFocus: false, restoreFocus: false }"
2122
:aria-hidden="!isOpen ? 'true' : 'false'"
2223
class="d-modal"
2324
data-qa="dt-modal"
@@ -72,7 +73,6 @@
7273
</template>
7374

7475
<script>
75-
import Modal from '@/common/mixins/modal';
7676
import ModeMixin from '@/common/mixins/mode';
7777
import { returnFirstEl } from '@/common/utils';
7878
import { EVENT_KEYNAMES } from '@/common/constants';
@@ -90,7 +90,7 @@ export default {
9090
DtIconClose,
9191
},
9292
93-
mixins: [Modal, ModeMixin],
93+
mixins: [ModeMixin],
9494
9595
props: {
9696
/**
@@ -181,14 +181,12 @@ export default {
181181
},
182182
183183
keydown: event => {
184+
// Tab focus trapping is handled by the v-dt-focustrap directive on the modal.
184185
switch (event.code) {
185186
case EVENT_KEYNAMES.esc:
186187
case EVENT_KEYNAMES.escape:
187188
this.close();
188189
break;
189-
case EVENT_KEYNAMES.tab:
190-
this.trapFocus(event);
191-
break;
192190
}
193191
},
194192
};
@@ -252,12 +250,6 @@ export default {
252250
focusAfterOpen () {
253251
returnFirstEl(this.$refs.closeImage?.$el)?.focus();
254252
},
255-
256-
trapFocus (e) {
257-
if (this.isOpen) {
258-
this.focusTrappedTabPress(e);
259-
}
260-
},
261253
},
262254
};
263255
</script>

0 commit comments

Comments
 (0)