refactor(datepicker): optimize focus handling and improve calendar positioning logic

This commit is contained in:
Matej Glasnák committed 2025-12-03 13:12:46 +01:00
1 parent a1e71006f0
commit 3e130c9a0d
3 files changed
+88 -169

No files matched your search

+88 -88
View File
@@ -61,9 +61,8 @@ export default {
mounted() { mounted() {
// Attach focus listeners on mount to catch any focus events // Attach focus listeners on mount to catch any focus events
this.$nextTick(() => { // Use immediate attachment (no $nextTick) to catch early focus events
this.attachFocusListeners(); this.attachFocusListeners();
});
}, },
methods: { methods: {
@@ -72,47 +71,36 @@ export default {
}, },
handleOpen(event) { handleOpen(event) {
console.log(
'🔵 [z-datepicker] handleOpen called at:',
new Date().toISOString(),
);
this.isFocused = true; this.isFocused = true;
// IMPORTANT: Set flag and save position BEFORE any other action // IMPORTANT: If scroll position wasn't saved yet (handleClick wasn't called),
// save it now but it might be too late and already scrolled to 0
if (this.savedScrollPosition === null) {
this.preventInputFocus = true; this.preventInputFocus = true;
console.log(
'🔵 [z-datepicker] preventInputFocus set to:',
this.preventInputFocus,
);
// Save scroll position
const modalContent = document.querySelector('.v-dialog .modal-content'); const modalContent = document.querySelector('.v-dialog .modal-content');
if (modalContent) { if (modalContent) {
this.savedScrollPosition = modalContent.scrollTop; this.savedScrollPosition = modalContent.scrollTop;
console.log(
'🔵 [z-datepicker] Saved scroll position:',
this.savedScrollPosition,
);
} else {
console.log('🔵 [z-datepicker] Modal content not found');
} }
}
// CRITICAL: Blur any currently focused input (v-dialog autofocus issue)
this.blurFocusedInputs();
// Reattach focus event listeners (for dynamically added inputs) // Reattach focus event listeners (for dynamically added inputs)
this.attachFocusListeners(); this.attachFocusListeners();
// Position the calendar menu relative to the modal
this.positionCalendar();
// Reset after datepicker is opened // Reset after datepicker is opened
setTimeout(() => { setTimeout(() => {
this.preventInputFocus = false; this.preventInputFocus = false;
this.savedScrollPosition = null; this.savedScrollPosition = null;
console.log(
'🔵 [z-datepicker] Reset preventInputFocus and savedScrollPosition after 500ms',
);
}, 500); }, 500);
// Emit to parent (for backward compatibility) // Emit to parent (for backward compatibility)
const wrapperElement = const wrapperElement =
event?.target?.closest?.('.z-datepicker-wrapper') || true; event?.target?.closest?.('.z-datepicker-wrapper') || true;
console.log('🔵 [z-datepicker] Emitting open event to parent');
this.$emit('open', wrapperElement); this.$emit('open', wrapperElement);
}, },
@@ -122,7 +110,6 @@ export default {
}, },
handleClick(event) { handleClick(event) {
console.log('🔵 [z-datepicker] Wrapper clicked');
// Prevent any default behavior // Prevent any default behavior
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -130,16 +117,16 @@ export default {
// CRITICAL: Set prevention flag BEFORE opening menu to catch early focus events // CRITICAL: Set prevention flag BEFORE opening menu to catch early focus events
this.preventInputFocus = true; this.preventInputFocus = true;
// Save scroll position immediately // Save scroll position FIRST (before blur or opening menu)
const modalContent = document.querySelector('.v-dialog .modal-content'); const modalContent = document.querySelector('.v-dialog .modal-content');
if (modalContent) { if (modalContent) {
this.savedScrollPosition = modalContent.scrollTop; this.savedScrollPosition = modalContent.scrollTop;
console.log(
'🔵 [z-datepicker] Pre-saved scroll position in handleClick:',
this.savedScrollPosition,
);
} }
// CRITICAL: Blur any currently focused input
this.blurFocusedInputs(false); // Pass false to not restore scroll yet
// Open the menu
if (this.$refs.datepickerRef?.openMenu) { if (this.$refs.datepickerRef?.openMenu) {
this.$refs.datepickerRef.openMenu(); this.$refs.datepickerRef.openMenu();
} }
@@ -151,23 +138,71 @@ export default {
} }
}, },
positionCalendar() {
setTimeout(() => {
const calendar = document.querySelector(
'.dp__menu.dp__menu_index.dp__theme_light',
);
if (!calendar) return;
// Get the datepicker wrapper element
const datepickerWrapper = this.$el;
// Get the modal container
const modalContainer = document.querySelector('.v-overlay__content');
if (!datepickerWrapper || !modalContainer) return;
// Get bounding rectangles
const wrapperRect = datepickerWrapper.getBoundingClientRect();
const modalRect = modalContainer.getBoundingClientRect();
// Calculate position relative to modal
const topPosition =
wrapperRect.top - modalRect.top + wrapperRect.height;
const leftPosition = wrapperRect.left - modalRect.left;
// Apply positioning
calendar.style.top = `${topPosition + 210}px`;
}, 100);
},
blurFocusedInputs(restoreScroll = true) {
const activeElement = document.activeElement;
if (activeElement && activeElement.tagName === 'INPUT') {
// Don't blur the datepicker's own input
if (
activeElement.closest('.dp__input_wrap') ||
activeElement.closest('.z-datepicker-wrapper')
) {
return;
}
activeElement.blur();
// Restore scroll position after blur (only if requested)
if (restoreScroll && this.savedScrollPosition !== null) {
const modalContent = document.querySelector(
'.v-dialog .modal-content',
);
if (modalContent) {
modalContent.scrollTop = this.savedScrollPosition;
}
}
}
},
attachFocusListeners() { attachFocusListeners() {
console.log('🔵 [z-datepicker] attachFocusListeners called');
// Remove old listeners first // Remove old listeners first
this.removeFocusListeners(); this.removeFocusListeners();
// Attach new listeners to all autocomplete inputs AND text fields // Attach new listeners to autocomplete inputs only (like the mixin)
const selectors = [ // Trying to catch all inputs causes issues with event timing
'.v-autocomplete input', const inputs = document.querySelectorAll('.v-autocomplete input');
'.v-text-field input',
'input[type="text"]',
];
let totalListeners = 0; let totalListeners = 0;
selectors.forEach((selector) => {
const inputs = document.querySelectorAll(selector);
let selectorCount = 0;
inputs.forEach((input) => { inputs.forEach((input) => {
// Skip the datepicker's own input // Skip the datepicker's own input
if (input.closest('.dp__input_wrap')) { if (input.closest('.dp__input_wrap')) {
@@ -176,81 +211,41 @@ export default {
const listener = this.onInputFocus.bind(this); const listener = this.onInputFocus.bind(this);
this.focusListeners.set(input, listener); this.focusListeners.set(input, listener);
input.addEventListener('focus', listener, true); // Use capture phase input.addEventListener('focus', listener);
selectorCount++;
totalListeners++; totalListeners++;
}); });
console.log(
`🔵 [z-datepicker] Found ${selectorCount} inputs for selector: ${selector}`,
);
});
console.log(
`🔵 [z-datepicker] Total focus listeners attached: ${totalListeners}`,
);
}, },
onInputFocus(event) { onInputFocus(event) {
const target = event.target; const target = event.target;
console.log('🔵 [z-datepicker] onInputFocus triggered on:', {
tagName: target.tagName,
type: target.type,
id: target.id,
className: target.className,
preventInputFocus: this.preventInputFocus,
savedScrollPosition: this.savedScrollPosition,
});
// Skip if it's the datepicker's own input // Skip if it's the datepicker's own input
if ( if (
target.closest('.dp__input_wrap') || target.closest('.dp__input_wrap') ||
target.closest('.z-datepicker-wrapper') target.closest('.z-datepicker-wrapper')
) { ) {
console.log(
"🔵 [z-datepicker] Skipping - this is datepicker's own input",
);
return; return;
} }
// If prevention flag is set, block the focus immediately // If prevention flag is set, block the focus
if (this.preventInputFocus) { if (this.preventInputFocus && this.savedScrollPosition !== null) {
console.log('🔵 [z-datepicker] PREVENTING FOCUS on:', target);
const scrollBefore = document.querySelector( const scrollBefore = document.querySelector(
'.v-dialog .modal-content', '.v-dialog .modal-content',
)?.scrollTop; )?.scrollTop;
console.log('🔵 [z-datepicker] Scroll position before:', scrollBefore);
// Prevent the focus - stop all propagation // Blur the focused element with a slight delay (like the mixin does)
event.preventDefault(); setTimeout(() => {
event.stopPropagation();
event.stopImmediatePropagation();
// Blur the focused element immediately (no timeout)
target.blur(); target.blur();
// Restore scroll position if we have one saved // Restore scroll position after blur
if (this.savedScrollPosition !== null) {
const modalContent = document.querySelector( const modalContent = document.querySelector(
'.v-dialog .modal-content', '.v-dialog .modal-content',
); );
if (modalContent) { if (modalContent && this.savedScrollPosition !== null) {
modalContent.scrollTop = this.savedScrollPosition; modalContent.scrollTop = this.savedScrollPosition;
const scrollAfter = modalContent.scrollTop; const scrollAfter = modalContent.scrollTop;
console.log(
'🔵 [z-datepicker] Scroll position restored to:',
scrollAfter,
);
} }
} }, 0);
} else {
console.log(
'🔵 [z-datepicker] Focus allowed (preventInputFocus:',
this.preventInputFocus,
', savedScrollPosition:',
this.savedScrollPosition,
')',
);
} }
}, },
@@ -319,6 +314,11 @@ export default {
:deep(.dp__input_wrap) :deep(.dp__input_wrap)
pointer-events: none pointer-events: none
cursor: pointer
:deep(.dp__input)
cursor: pointer
pointer-events: none !important
:deep(.dp__clear_icon) :deep(.dp__clear_icon)
pointer-events: auto !important pointer-events: auto !important
@@ -10,19 +10,9 @@ export default {
methods: { methods: {
setFocusEvent() { setFocusEvent() {
const inputs = document.querySelectorAll('.v-autocomplete input'); const inputs = document.querySelectorAll('.v-autocomplete input');
console.log(
'🟢 [VuetifyFixFocusMixin] setFocusEvent - Found',
inputs.length,
'autocomplete inputs',
);
inputs.forEach((input) => { inputs.forEach((input) => {
input.addEventListener('focus', this.onInputFocus); input.addEventListener('focus', this.onInputFocus);
}); });
console.log(
'🟢 [VuetifyFixFocusMixin] Attached focus listeners to',
inputs.length,
'inputs',
);
}, },
onDateFocus($event) { onDateFocus($event) {
@@ -32,53 +22,26 @@ export default {
}, },
onDateOpen($event) { onDateOpen($event) {
console.log(
'🟢 [VuetifyFixFocusMixin] onDateOpen called at:',
new Date().toISOString(),
'with event:',
$event,
);
// $event should be the element reference (or any truthy value to trigger saving) // $event should be the element reference (or any truthy value to trigger saving)
if ($event) { if ($event) {
// Find the scrollable modal container // Find the scrollable modal container
const modalContent = document.querySelector('.v-dialog .modal-content'); const modalContent = document.querySelector('.v-dialog .modal-content');
if (modalContent) { if (modalContent) {
this.savedScrollPosition = modalContent.scrollTop; this.savedScrollPosition = modalContent.scrollTop;
console.log(
'🟢 [VuetifyFixFocusMixin] Saved scroll position:',
this.savedScrollPosition,
);
} else {
console.log('🟢 [VuetifyFixFocusMixin] Modal content not found');
} }
this.preventInputFocus = true; this.preventInputFocus = true;
console.log(
'🟢 [VuetifyFixFocusMixin] preventInputFocus set to:',
this.preventInputFocus,
);
// Reset after datepicker is opened // Reset after datepicker is opened
setTimeout(() => { setTimeout(() => {
this.preventInputFocus = false; this.preventInputFocus = false;
this.savedScrollPosition = null; this.savedScrollPosition = null;
console.log(
'🟢 [VuetifyFixFocusMixin] Reset preventInputFocus and savedScrollPosition after 500ms',
);
}, 500); }, 500);
} }
}, },
onInputFocus($event) { onInputFocus($event) {
const target = $event.target; const target = $event.target;
console.log('🟢 [VuetifyFixFocusMixin] onInputFocus triggered on:', {
tagName: target.tagName,
type: target.type,
id: target.id,
className: target.className,
preventInputFocus: this.preventInputFocus,
savedScrollPosition: this.savedScrollPosition,
});
// If we have a saved scroll position and an unwanted focus happened // If we have a saved scroll position and an unwanted focus happened
if (this.preventInputFocus && this.savedScrollPosition !== null) { if (this.preventInputFocus && this.savedScrollPosition !== null) {
@@ -86,14 +49,9 @@ export default {
// Only intercept text input focus (autocomplete fields) // Only intercept text input focus (autocomplete fields)
if (target.tagName === 'INPUT' && target.type === 'text') { if (target.tagName === 'INPUT' && target.type === 'text') {
console.log('🟢 [VuetifyFixFocusMixin] PREVENTING FOCUS on:', target);
const scrollBefore = document.querySelector( const scrollBefore = document.querySelector(
'.v-dialog .modal-content', '.v-dialog .modal-content',
)?.scrollTop; )?.scrollTop;
console.log(
'🟢 [VuetifyFixFocusMixin] Scroll position before:',
scrollBefore,
);
// Blur the focused element // Blur the focused element
setTimeout(() => { setTimeout(() => {
@@ -105,25 +63,9 @@ export default {
if (modalContent && this.savedScrollPosition !== null) { if (modalContent && this.savedScrollPosition !== null) {
modalContent.scrollTop = this.savedScrollPosition; modalContent.scrollTop = this.savedScrollPosition;
const scrollAfter = modalContent.scrollTop; const scrollAfter = modalContent.scrollTop;
console.log(
'🟢 [VuetifyFixFocusMixin] Scroll position restored to:',
scrollAfter,
);
} }
}, 0); }, 0);
} else {
console.log(
'🟢 [VuetifyFixFocusMixin] Not a text input, skipping prevention',
);
} }
} else {
console.log(
'🟢 [VuetifyFixFocusMixin] Focus allowed (preventInputFocus:',
this.preventInputFocus,
', savedScrollPosition:',
this.savedScrollPosition,
')',
);
} }
}, },
}, },
@@ -505,12 +505,6 @@ export default {
methods: { methods: {
// Override onDateOpen from VuetifyFixFocusMixin to add logging // Override onDateOpen from VuetifyFixFocusMixin to add logging
onDateOpen($event) { onDateOpen($event) {
console.log(
'🟡 [z-task-modal] onDateOpen called at:',
new Date().toISOString(),
'with event:',
$event,
);
// Call the parent mixin's onDateOpen method // Call the parent mixin's onDateOpen method
if ( if (
VuetifyFixFocusMixin.methods && VuetifyFixFocusMixin.methods &&
@@ -523,17 +517,6 @@ export default {
// Override onInputFocus from VuetifyFixFocusMixin to add logging // Override onInputFocus from VuetifyFixFocusMixin to add logging
onInputFocus($event) { onInputFocus($event) {
const target = $event?.target; const target = $event?.target;
console.log(
'🟡 [z-task-modal] onInputFocus called at:',
new Date().toISOString(),
'on:',
{
tagName: target?.tagName,
type: target?.type,
id: target?.id,
className: target?.className,
},
);
// Call the parent mixin's onInputFocus method // Call the parent mixin's onInputFocus method
if ( if (
VuetifyFixFocusMixin.methods && VuetifyFixFocusMixin.methods &&
@@ -601,9 +584,6 @@ export default {
// CRITICAL: Prevent v-dialog autofocus BEFORE modal opens // CRITICAL: Prevent v-dialog autofocus BEFORE modal opens
this.preventInputFocus = true; this.preventInputFocus = true;
console.log(
'🟡 [z-task-modal] showModal - preventInputFocus set to true BEFORE modal opens',
);
this.visible = true; this.visible = true;
this.timeblocks = [ this.timeblocks = [
@@ -620,9 +600,6 @@ export default {
// Reset after modal is fully rendered and v-dialog autofocus has been blocked // Reset after modal is fully rendered and v-dialog autofocus has been blocked
setTimeout(() => { setTimeout(() => {
this.preventInputFocus = false; this.preventInputFocus = false;
console.log(
'🟡 [z-task-modal] showModal - preventInputFocus reset to false after 300ms',
);
}, 300); }, 300);
}); });