1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 10:54:44 +00:00

feat(packages/vue): add focusGuard composable

This commit is contained in:
2024-11-21 16:49:59 +07:00
parent 50257463b7
commit e4905ef87e
3 changed files with 110 additions and 1 deletions

View File

@@ -0,0 +1,69 @@
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { defineComponent, nextTick } from 'vue';
import { useFocusGuard } from '.';
const setupFocusGuard = (namespace?: string) => {
return mount(
defineComponent({
setup() {
useFocusGuard(namespace);
},
template: '<div></div>',
})
);
};
const getFocusGuards = (namespace: string) =>
document.querySelectorAll(`[data-${namespace}]`);
describe('useFocusGuard', () => {
let component: ReturnType<typeof setupFocusGuard>;
const namespace = 'test-guard';
beforeEach(() => {
document.body.innerHTML = '';
});
afterEach(() => {
component.unmount();
});
it('create focus guards when mounted', async () => {
component = setupFocusGuard(namespace);
const guards = getFocusGuards(namespace);
expect(guards.length).toBe(2);
guards.forEach((guard) => {
expect(guard.getAttribute('tabindex')).toBe('0');
expect(guard.getAttribute('style')).toContain('opacity: 0');
});
});
it('remove focus guards when unmounted', () => {
component = setupFocusGuard(namespace);
component.unmount();
expect(getFocusGuards(namespace).length).toBe(0);
});
it('correctly manage multiple instances with the same namespace', () => {
const wrapper1 = setupFocusGuard(namespace);
const wrapper2 = setupFocusGuard(namespace);
// Guards should not be duplicated
expect(getFocusGuards(namespace).length).toBe(2);
wrapper1.unmount();
// Second instance still keeps the guards
expect(getFocusGuards(namespace).length).toBe(2);
wrapper2.unmount();
// No guards left after all instances are unmounted
expect(getFocusGuards(namespace).length).toBe(0);
});
});

View File

@@ -0,0 +1,40 @@
import { focusGuard } from '@robonen/platform/browsers';
import { onMounted, onUnmounted } from 'vue';
// Global counter to drop the focus guards when the last instance is unmounted
let counter = 0;
/**
* @name useFocusGuard
* @category Utilities
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
*
* @param {string} [namespace] - A namespace to group the focus guards
* @returns {void}
*
* @example
* useFocusGuard();
*
* @example
* useFocusGuard('my-namespace');
*
* @since 0.0.2
*/
export function useFocusGuard(namespace?: string) {
const manager = focusGuard(namespace);
const createGuard = () => {
manager.createGuard();
counter++;
};
const removeGuard = () => {
if (counter <= 1)
manager.removeGuard();
counter = Math.max(0, counter - 1);
};
onMounted(createGuard);
onUnmounted(removeGuard);
}

View File

@@ -1,3 +1,3 @@
import { isClient } from '@robonen/platform'; import { isClient } from '@robonen/platform/multi';
export const defaultWindow = /* #__PURE__ */ isClient ? window : undefined export const defaultWindow = /* #__PURE__ */ isClient ? window : undefined