From 50257463b7aac0c301c930e964cbcd88e637957d Mon Sep 17 00:00:00 2001 From: robonen Date: Wed, 20 Nov 2024 16:49:24 +0700 Subject: [PATCH] feat(packages/platform): add focusGuard brwoser util --- .../src/browsers/focusGuard/index.test.ts | 69 +++++++++++++++++++ .../platform/src/browsers/focusGuard/index.ts | 50 ++++++++++++++ packages/platform/src/browsers/index.ts | 1 + 3 files changed, 120 insertions(+) create mode 100644 packages/platform/src/browsers/focusGuard/index.test.ts create mode 100644 packages/platform/src/browsers/focusGuard/index.ts create mode 100644 packages/platform/src/browsers/index.ts diff --git a/packages/platform/src/browsers/focusGuard/index.test.ts b/packages/platform/src/browsers/focusGuard/index.test.ts new file mode 100644 index 0000000..22e29a9 --- /dev/null +++ b/packages/platform/src/browsers/focusGuard/index.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { focusGuard, createGuardAttrs } from '.'; + +describe('focusGuard', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('initialize with the correct default namespace', () => { + const guard = focusGuard(); + + expect(guard.selector).toBe('data-focus-guard'); + }); + + it('create focus guards in the DOM', () => { + const guard = focusGuard(); + guard.createGuard(); + + const guards = document.querySelectorAll(`[${guard.selector}]`); + expect(guards.length).toBe(2); + + guards.forEach((element) => { + expect(element.tagName).toBe('SPAN'); + expect(element.getAttribute('tabindex')).toBe('0'); + }); + }); + + it('remove focus guards from the DOM correctly', () => { + const guard = focusGuard(); + guard.createGuard(); + guard.removeGuard(); + + const guards = document.querySelectorAll(`[${guard.selector}]`); + + expect(guards.length).toBe(0); + }); + + it('reuse the same guards when calling createGuard multiple times', () => { + const guard = focusGuard(); + guard.createGuard(); + guard.createGuard(); + + guard.removeGuard(); + const guards = document.querySelectorAll(`[${guard.selector}]`); + + expect(guards.length).toBe(0); + }); + + it('allow custom namespaces', () => { + const namespace = 'custom-guard'; + const guard = focusGuard(namespace); + guard.createGuard(); + + expect(guard.selector).toBe(`data-${namespace}`); + + const guards = document.querySelectorAll(`[${guard.selector}]`); + expect(guards.length).toBe(2); + }); + + it('createGuardAttrs should create a valid guard element', () => { + const namespace = 'custom-guard'; + const element = createGuardAttrs(namespace); + + expect(element.tagName).toBe('SPAN'); + expect(element.getAttribute(namespace)).toBe(''); + expect(element.getAttribute('tabindex')).toBe('0'); + expect(element.getAttribute('style')).toBe('outline: none; opacity: 0; pointer-events: none; position: fixed;'); + }); +}); \ No newline at end of file diff --git a/packages/platform/src/browsers/focusGuard/index.ts b/packages/platform/src/browsers/focusGuard/index.ts new file mode 100644 index 0000000..f4d7ec0 --- /dev/null +++ b/packages/platform/src/browsers/focusGuard/index.ts @@ -0,0 +1,50 @@ +/** + * @name focusGuard + * @category Browsers + * @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior + * + * @param {string} namespace - The namespace to use for the guard attributes + * @returns {Object} - An object containing the selector, createGuard, and removeGuard functions + * + * @example + * const guard = focusGuard(); + * guard.createGuard(); + * guard.removeGuard(); + * + * @example + * const guard = focusGuard('focus-guard'); + * guard.createGuard(); + * guard.removeGuard(); + * + * @since 0.0.3 + */ +export function focusGuard(namespace: string = 'focus-guard') { + const guardAttr = `data-${namespace}`; + + const createGuard = () => { + const edges = document.querySelectorAll(`[${guardAttr}]`); + + document.body.insertAdjacentElement('afterbegin', edges[0] ?? createGuardAttrs(guardAttr)); + document.body.insertAdjacentElement('beforeend', edges[1] ?? createGuardAttrs(guardAttr)); + }; + + const removeGuard = () => { + document.querySelectorAll(`[${guardAttr}]`).forEach((element) => element.remove()); + }; + + return { + selector: guardAttr, + createGuard, + removeGuard, + }; +} + +export function createGuardAttrs(namespace: string) { + const element = document.createElement('span'); + + element.setAttribute(namespace, ''); + element.setAttribute('tabindex', '0'); + element.setAttribute('style', 'outline: none; opacity: 0; pointer-events: none; position: fixed;'); + + return element; +} \ No newline at end of file diff --git a/packages/platform/src/browsers/index.ts b/packages/platform/src/browsers/index.ts new file mode 100644 index 0000000..0569ecb --- /dev/null +++ b/packages/platform/src/browsers/index.ts @@ -0,0 +1 @@ +export * from './focusGuard'; \ No newline at end of file