Skip to content

Commit 589edb5

Browse files
refactor(modal): DLT-3262 migrate DtModal to native dialog element (#1179)
1 parent 1697911 commit 589edb5

File tree

6 files changed

+172
-127
lines changed

6 files changed

+172
-127
lines changed

packages/dialtone-css/lib/build/less/components/modal.less

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,37 @@
119119
will-change: visibility, z-index, opacity;
120120
}
121121

122+
// $$ DIALOG ELEMENT RESET
123+
// ----------------------------------------------------------------------------
124+
// When used as a native <dialog>, reset UA styles and hide the native backdrop
125+
// since .d-modal itself serves as the visual backdrop overlay.
126+
// The :not([open]) rule restores UA hidden behavior that .d-modal's display: flex
127+
// would otherwise override.
128+
:where(dialog).d-modal {
129+
margin: 0;
130+
color: inherit;
131+
border: none;
132+
inline-size: auto;
133+
block-size: auto;
134+
max-inline-size: none;
135+
max-block-size: none;
136+
137+
&:not([open]) {
138+
display: none;
139+
}
140+
141+
&::backdrop {
142+
background: transparent;
143+
}
144+
}
145+
122146
.d-modal--transparent {
123147
--modal-backdrop-color-background: var(--d-bgc-transparent);
124148

125149
// If we don't set this the native app header region will override all click events on the modal overlay
126150
-webkit-app-region: no-drag;
127151

128-
&[aria-hidden='false'] {
152+
&:is([aria-hidden='false'], [open]) {
129153
position: fixed;
130154
inset-block-start: 0;
131155
inset-inline-start: 0;
@@ -169,10 +193,10 @@
169193

170194
// $$ MAKE THEM APPEAR
171195
// ----------------------------------------------------------------------------
172-
.d-modal[aria-hidden='false'],
173-
.d-modal[aria-hidden='false'] .d-modal__dialog,
174-
.d-modal--transparent[aria-hidden='false'],
175-
.d-modal--transparent[aria-hidden='false'] .d-modal__dialog {
196+
// [aria-hidden='false'] — raw HTML usage (div-based)
197+
// [open] — native <dialog> element usage
198+
:is(.d-modal, .d-modal--transparent):is([aria-hidden='false'], [open]),
199+
:is(.d-modal, .d-modal--transparent):is([aria-hidden='false'], [open]) .d-modal__dialog {
176200
z-index: var(--zi-modal);
177201
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
178202
visibility: visible;

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ const baseSlots = {};
2222
let mockProps = {};
2323
let mockSlots = {};
2424

25+
// Mock native <dialog> methods not supported in JSDOM
26+
beforeAll(() => {
27+
HTMLDialogElement.prototype.showModal = vi.fn(function () {
28+
this.setAttribute('open', '');
29+
});
30+
HTMLDialogElement.prototype.close = vi.fn(function () {
31+
this.removeAttribute('open');
32+
});
33+
});
34+
35+
afterAll(() => {
36+
delete HTMLDialogElement.prototype.showModal;
37+
delete HTMLDialogElement.prototype.close;
38+
});
39+
2540
describe('DtModal Tests', () => {
2641
let wrapper;
2742
let closeBtn;
@@ -47,6 +62,7 @@ describe('DtModal Tests', () => {
4762
};
4863

4964
beforeEach(() => {
65+
vi.clearAllMocks();
5066
updateWrapper();
5167
});
5268

@@ -56,17 +72,15 @@ describe('DtModal Tests', () => {
5672
wrapper.unmount();
5773
});
5874

59-
afterAll(() => {
60-
// Restore RequestAnimationFrame and cancelAnimationFrame
61-
global.requestAnimationFrame = undefined;
62-
global.cancelAnimationFrame = undefined;
63-
});
64-
6575
describe('Presentation Tests', () => {
6676
it('should render the component', () => {
6777
expect(wrapper.exists()).toBe(true);
6878
});
6979

80+
it('should render using a native dialog element', () => {
81+
expect(overlay.element.tagName).toBe('DIALOG');
82+
});
83+
7084
it('should render the title content', () => {
7185
expect(title.exists()).toBe(true);
7286
expect(title.text()).toEqual(MOCK_MODAL_TITLE);
@@ -94,6 +108,10 @@ describe('DtModal Tests', () => {
94108
expect(banner.classes(MODAL_BANNER_KINDS[DtModal.props.bannerKind.default])).toBe(true);
95109
});
96110

111+
it('Should call showModal when show is true', () => {
112+
expect(overlay.element.showModal).toHaveBeenCalled();
113+
});
114+
97115
describe('When hideClose prop is true', () => {
98116
beforeEach(async () => {
99117
mockProps = { ...mockProps, hideClose: true };
@@ -162,14 +180,20 @@ describe('DtModal Tests', () => {
162180
expect(wrapper.emitted()[SYNC_EVENT_NAME][0][0]).toBe(false);
163181
});
164182

165-
it('Should emit a sync-able update event when escape key is pressed', async () => {
183+
it('Should emit a sync-able update event when cancel event fires (escape key)', async () => {
166184
expect(wrapper.emitted(SYNC_EVENT_NAME)).toBeFalsy();
167185

168-
await overlay.trigger('keydown', { code: 'Escape' });
186+
await overlay.trigger('cancel');
169187

170188
expect(wrapper.emitted()[SYNC_EVENT_NAME].length).toBe(1);
171189
expect(wrapper.emitted()[SYNC_EVENT_NAME][0][0]).toBe(false);
172190
});
191+
192+
it('Should emit keydown event to parent', async () => {
193+
await overlay.trigger('keydown', { code: 'Escape' });
194+
195+
expect(wrapper.emitted().keydown).toBeTruthy();
196+
});
173197
});
174198

175199
describe('Extendability Tests', () => {

0 commit comments

Comments
 (0)