Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/components/EventForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export class EventForm extends BaseComponent {
background: var(--fc-background);
width: 400px;
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
border-radius: var(--fc-border-radius-lg);
box-shadow: var(--fc-shadow-lg);
border: 1px solid var(--fc-border-color);
Expand All @@ -78,6 +80,7 @@ export class EventForm extends BaseComponent {
}

.modal-header {
flex-shrink: 0;
padding: var(--fc-spacing-lg);
border-bottom: 1px solid var(--fc-border-color);
display: flex;
Expand Down Expand Up @@ -109,13 +112,16 @@ export class EventForm extends BaseComponent {
}

.modal-body {
min-height: 0;
overflow-y: auto;
padding: var(--fc-spacing-lg);
display: flex;
flex-direction: column;
gap: var(--fc-spacing-md);
}

.form-group {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
Expand All @@ -130,6 +136,8 @@ export class EventForm extends BaseComponent {
input[type="text"],
input[type="datetime-local"],
select {
width: 100%;
min-width: 0;
padding: 8px 12px;
border: 1px solid var(--fc-border-color);
border-radius: var(--fc-border-radius);
Expand All @@ -149,6 +157,7 @@ export class EventForm extends BaseComponent {

.row {
display: flex;
flex-direction: column;
gap: var(--fc-spacing-md);
}

Expand All @@ -157,6 +166,7 @@ export class EventForm extends BaseComponent {
}

.modal-footer {
flex-shrink: 0;
padding: var(--fc-spacing-lg);
border-top: 1px solid var(--fc-border-color);
display: flex;
Expand All @@ -170,6 +180,7 @@ export class EventForm extends BaseComponent {
/* Color picker style */
.color-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
}
Expand Down Expand Up @@ -299,9 +310,10 @@ export class EventForm extends BaseComponent {
});
});

// Close on backdrop click
// Shadow DOM retargets inside clicks to the host. Only dismiss when the
// composed path does not pass through the dialog, including nested shadows.
this.addListener(this, 'click', e => {
if (e.target === this) this.close();
if (!e.composedPath().includes(this.modalContent)) this.close();
});

// Close on Escape key - only add once to prevent memory leaks
Expand Down
84 changes: 84 additions & 0 deletions tests/unit/EventForm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,88 @@ describe('EventForm', () => {

form.save();
});

describe('composed click interactions', () => {
const click = element => element.dispatchEvent(new MouseEvent('click', {
bubbles: true,
composed: true
}));

test.each(['#event-title', '#event-start', '#event-end', '.modal-content']) (
'keeps the dialog open when clicking %s across the shadow boundary', selector => {
form.open();
const received = jest.fn();
form.addEventListener('click', received);

click(form.$(selector));

// A real composed event is retargeted to the host, even for an inside click.
expect(received).toHaveBeenCalledTimes(1);
expect(received.mock.calls[0][0].target).toBe(form);
expect(form.hasAttribute('open')).toBe(true);
}
);

test('selects a color without dismissing or saving, then saves that color', () => {
form.open();
form.titleInput.value = 'Release planning';
const onSave = jest.fn();
form.addEventListener('save', onSave);
const purple = form.$('[aria-label="Purple"]');

click(purple);

expect(form.hasAttribute('open')).toBe(true);
expect(purple.getAttribute('aria-checked')).toBe('true');
expect(form.titleInput.value).toBe('Release planning');
expect(onSave).not.toHaveBeenCalled();

click(form.$('#save-btn'));

expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave.mock.calls[0][0].detail.backgroundColor).toBe(purple.dataset.color);
expect(form.hasAttribute('open')).toBe(false);
});

test.each(['title', 'date range'])('keeps invalid %s errors visible after Save', invalidField => {
form.open();
form.titleInput.value = invalidField === 'title' ? '' : 'Release planning';
if (invalidField === 'date range') form.endInput.value = form.startInput.value;
const onSave = jest.fn();
form.addEventListener('save', onSave);

click(form.$('#save-btn'));

expect(form.hasAttribute('open')).toBe(true);
expect(form.$(invalidField === 'title' ? '#title-group' : '#end-group')
.classList.contains('has-error')).toBe(true);
expect(onSave).not.toHaveBeenCalled();
});

test.each(['#cancel-btn', '#close-x', '#close-x path'])('still dismisses with %s', selector => {
form.open();
click(form.$(selector));
expect(form.hasAttribute('open')).toBe(false);
});

test('closes only on the backdrop after repeated opening inside another shadow root', () => {
const outer = document.createElement('div');
const root = outer.attachShadow({ mode: 'open' });
document.body.appendChild(outer);
root.appendChild(form);
try {
for (let attempt = 0; attempt < 2; attempt++) {
form.open();
click(form.$('[aria-label="Purple"]'));
expect(form.hasAttribute('open')).toBe(true);
click(form);
expect(form.hasAttribute('open')).toBe(false);
}
} finally {
document.body.appendChild(form);
outer.remove();
}
});
});

});
Loading