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:
69
packages/vue/src/composables/useFocusGuard/index.test.ts
Normal file
69
packages/vue/src/composables/useFocusGuard/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
40
packages/vue/src/composables/useFocusGuard/index.ts
Normal file
40
packages/vue/src/composables/useFocusGuard/index.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user