Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Re-exposed the activate/deactivate methods on component #375

Merged
merged 1 commit into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cypress/integration/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down Expand Up @@ -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')
})
})
})
38 changes: 38 additions & 0 deletions demo/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,41 @@
</button>
</p>
</section>

<section id="methods">
<h2 id="methods-heading">exposed methods</h2>
<p>
Uses the methods exposed by the component (via $refs attachment) to activate and deactivate the focus trap
</p>
<p>
<button @click="() => $refs.methodsFocusTrap.activate()">activate trap</button>
</p>

<focus-trap
ref="methodsFocusTrap"
v-model:active="demos.methods.isActive"
>
<div class="trap" :class="demos.methods.isActive && 'is-active'">
<p>
Here is a focus trap
<a href="#">with</a>
<a href="#">some</a>
<a href="#">focusable</a> parts.
</p>
<p>
<label class="inline-label">
Initially focused input
<input ref="ieneInput" />
</label>
</p>
<p>
<button @click="() => $refs.methodsFocusTrap.deactivate()">
deactivate trap via method call
</button>
</p>
</div>
</focus-trap>
</section>
</div>
</template>

Expand Down Expand Up @@ -252,6 +287,9 @@ export default {
isActive: false,
clickOutsideEnabled: false,
allowOutsideClick: () => this.demos.aoc.clickOutsideEnabled
},
methods: {
isActive: false,
}
},
}
Expand Down
74 changes: 48 additions & 26 deletions src/FocusTrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from 'vue'
import {
createFocusTrap,
FocusTarget,
FocusTrap as FocusTrapI,
MouseEventToBoolean,
} from 'focus-trap'
Expand Down Expand Up @@ -38,7 +39,7 @@ export const FocusTrap = defineComponent({
default: false,
},
initialFocus: {
type: [String, Function] as PropType<string | (() => HTMLElement)>,
type: [String, Function] as PropType<FocusTarget>,
},
fallbackFocus: {
type: [String, Function] as PropType<string | (() => HTMLElement)>,
Expand All @@ -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<HTMLElement | null>(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()
Expand All @@ -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

Expand Down