Implement custom lightweight modal #230
Introduce a brand new lightweight and efficient modal component. It is designed to be visually similar to the previous one to not introduce a change in feel of the application in a patch release, but behind the scenes it features: - Enhanced application speed and reduced bundle size. - New flexbox-driven layout, eliminating JS calculations. - Composition API ready for Vue 3.0 #230. Other changes: - Adopt idiomatic Vue via `v-modal` binding. - Add unit tests for both the modal and dialog. - Remove `vue-js-modal` dependency in favor of the new implementation. - Adjust modal shadow color to better match theme. - Add `@vue/test-utils` for unit testing.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { Ref, computed, watch } from 'vue';
|
||||
|
||||
/**
|
||||
* This function monitors a set of conditions (represented as refs) and
|
||||
* maintains a composite status based on all conditions.
|
||||
*/
|
||||
export function useAllTrueWatcher(
|
||||
...conditions: Ref<boolean>[]
|
||||
) {
|
||||
const allMetCallbacks = new Array<() => void>();
|
||||
|
||||
const areAllConditionsMet = computed(() => conditions.every((condition) => condition.value));
|
||||
|
||||
watch(areAllConditionsMet, (areMet) => {
|
||||
if (areMet) {
|
||||
allMetCallbacks.forEach((action) => action());
|
||||
}
|
||||
});
|
||||
|
||||
function resetAllConditions() {
|
||||
conditions.forEach((condition) => {
|
||||
condition.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onAllConditionsMet(callback: () => void) {
|
||||
allMetCallbacks.push(callback);
|
||||
if (areAllConditionsMet.value) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
resetAllConditions,
|
||||
onAllConditionsMet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Ref, watchEffect } from 'vue';
|
||||
|
||||
/**
|
||||
* Manages focus transitions, ensuring good usability and accessibility.
|
||||
*/
|
||||
export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
|
||||
let previouslyFocusedElement: HTMLElement | undefined;
|
||||
|
||||
watchEffect(() => {
|
||||
if (shouldDisableFocus.value) {
|
||||
previouslyFocusedElement = document.activeElement as HTMLElement | null;
|
||||
previouslyFocusedElement?.blur();
|
||||
} else {
|
||||
if (!previouslyFocusedElement || previouslyFocusedElement.tagName === 'BODY') {
|
||||
// It doesn't make sense to return focus to the body after the modal is
|
||||
// closed because the body itself doesn't offer meaningful interactivity
|
||||
return;
|
||||
}
|
||||
previouslyFocusedElement.focus();
|
||||
previouslyFocusedElement = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { onBeforeMount, onBeforeUnmount } from 'vue';
|
||||
|
||||
export function useEscapeKeyListener(callback: () => void) {
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Ref, watch, onBeforeUnmount } from 'vue';
|
||||
|
||||
/*
|
||||
It blocks background scrolling.
|
||||
Designed to be used by modals, overlays etc.
|
||||
*/
|
||||
export function useLockBodyBackgroundScroll(isActive: Ref<boolean>) {
|
||||
const originalStyles = {
|
||||
overflow: document.body.style.overflow,
|
||||
width: document.body.style.width,
|
||||
};
|
||||
|
||||
const block = () => {
|
||||
originalStyles.overflow = document.body.style.overflow;
|
||||
originalStyles.width = document.body.style.width;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.width = '100vw';
|
||||
};
|
||||
|
||||
const unblock = () => {
|
||||
document.body.style.overflow = originalStyles.overflow;
|
||||
document.body.style.width = originalStyles.width;
|
||||
};
|
||||
|
||||
watch(isActive, (shouldBlock) => {
|
||||
if (shouldBlock) {
|
||||
block();
|
||||
} else {
|
||||
unblock();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unblock();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user