diff --git a/cypress/integration/index.spec.js b/cypress/integration/index.spec.js index 3feef1e..df50e65 100644 --- a/cypress/integration/index.spec.js +++ b/cypress/integration/index.spec.js @@ -15,6 +15,14 @@ function activateTrapWithButton(id) { .click() } +function deactivateTrapWithButton(id) { + cy.get(`${id} .trap`) + .should('have.class', 'is-active') + .get(`${id} .trap > p > button`) + .first() + .click() +} + function assertActivatedTrap(id) { cy.get(`${id} .trap`).should('have.class', 'is-active') } @@ -119,4 +127,14 @@ describe('focus trap vue', () => { assertActivatedTrap('#basic') }) }) + + describe('method control of focus trap', () => { + it('allows control of trap via exposed methods', () => { + activateTrapWithButton('#methods') + assertActivatedTrap('#methods') + + deactivateTrapWithButton('#methods') + assertDeactivatedTrap('#methods') + }) + }) }) diff --git a/demo/App.vue b/demo/App.vue index bca79de..47f41b8 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -224,6 +224,41 @@

+ +
+

exposed methods

+

+ Uses the methods exposed by the component (via $refs attachment) to activate and deactivate the focus trap +

+

+ +

+ + +
+

+ Here is a focus trap + with + some + focusable parts. +

+

+ +

+

+ +

+
+
+
@@ -252,6 +287,9 @@ export default { isActive: false, clickOutsideEnabled: false, allowOutsideClick: () => this.demos.aoc.clickOutsideEnabled + }, + methods: { + isActive: false, } }, } diff --git a/src/FocusTrap.ts b/src/FocusTrap.ts index edd6ebe..10b7349 100644 --- a/src/FocusTrap.ts +++ b/src/FocusTrap.ts @@ -10,6 +10,7 @@ import { } from 'vue' import { createFocusTrap, + FocusTarget, FocusTrap as FocusTrapI, MouseEventToBoolean, } from 'focus-trap' @@ -38,7 +39,7 @@ export const FocusTrap = defineComponent({ default: false, }, initialFocus: { - type: [String, Function] as PropType HTMLElement)>, + type: [String, Function] as PropType, }, fallbackFocus: { type: [String, Function] as PropType HTMLElement)>, @@ -47,40 +48,50 @@ export const FocusTrap = defineComponent({ emits: ['update:active', 'activate', 'deactivate'], - setup(props, { slots, emit }) { + setup(props, { slots, emit, expose }) { let trap: FocusTrapI | null const el = ref(null) + const ensureTrap = () => { + if (trap) { + return + } + + const { initialFocus } = props + trap = createFocusTrap(el.value as HTMLElement, { + escapeDeactivates: props.escapeDeactivates, + allowOutsideClick: event => + typeof props.allowOutsideClick === 'function' + ? props.allowOutsideClick(event) + : props.allowOutsideClick, + returnFocusOnDeactivate: props.returnFocusOnDeactivate, + clickOutsideDeactivates: props.clickOutsideDeactivates, + onActivate: () => { + emit('update:active', true) + emit('activate') + }, + onDeactivate: () => { + emit('update:active', false) + emit('deactivate') + }, + initialFocus: initialFocus + ? typeof initialFocus === 'function' + ? initialFocus() + : initialFocus + : (el.value as HTMLElement), + fallbackFocus: props.fallbackFocus, + }) + } + onMounted(() => { watch( () => props.active, active => { - const { initialFocus } = props if (active && el.value) { // has no effect if already activated - trap = createFocusTrap(el.value, { - escapeDeactivates: props.escapeDeactivates, - allowOutsideClick: event => - typeof props.allowOutsideClick === 'function' - ? props.allowOutsideClick(event) - : props.allowOutsideClick, - returnFocusOnDeactivate: props.returnFocusOnDeactivate, - clickOutsideDeactivates: props.clickOutsideDeactivates, - onActivate: () => { - emit('update:active', true) - emit('activate') - }, - onDeactivate: () => { - emit('update:active', false) - emit('deactivate') - }, - initialFocus: initialFocus - ? typeof initialFocus === 'function' - ? initialFocus() - : initialFocus - : el.value, - fallbackFocus: props.fallbackFocus, - }) + ensureTrap() + + // @ts-ignore trap.activate() } else if (trap) { trap.deactivate() @@ -95,6 +106,17 @@ export const FocusTrap = defineComponent({ trap = null }) + expose({ + activate() { + ensureTrap() + // @ts-ignore + trap.activate() + }, + deactivate() { + trap && trap.deactivate() + }, + }) + return () => { if (!slots.default) return null