Skip to content

Commit

Permalink
feat(useFocusTrap): new function (#433)
Browse files Browse the repository at this point in the history
* feat(useFocusTrap): new function

* fix issue with focus trap not working when navigated to from other page

* Update packages/integrations/useFocusTrap/index.md

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>

* Update packages/integrations/useFocusTrap/index.ts

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>

* fix: update demo. Allow ref change for useFocusTrap

* fix: remove unnecessary stop

* fix: reflect foucs trap state on button

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
wheatjs and antfu committed Apr 8, 2021
1 parent 7945a85 commit d01cc66
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 0 deletions.
7 changes: 7 additions & 0 deletions indexes.json
Expand Up @@ -823,6 +823,13 @@
"category": "@Integrations",
"description": "wrapper for [`universal-cookie`](https://www.npmjs.com/package/universal-cookie)"
},
{
"name": "useFocusTrap",
"package": "integrations",
"docs": "https://vueuse.org/integrations/useFocusTrap/",
"category": "@Integrations",
"description": "reactive wrapper for [`useFocusTrap`](https://github.com/focus-trap/focus-trap)"
},
{
"name": "useJwt",
"package": "integrations",
Expand Down
1 change: 1 addition & 0 deletions packages/add-ons.md
Expand Up @@ -45,6 +45,7 @@ Utilities for vue-router
Integration wrappers for utility libraries
- [`useAxios`](https://vueuse.org/integrations/useAxios/) — wrapper for [`axios`](https://github.com/axios/axios)
- [`useCookies`](https://vueuse.org/integrations/useCookies/) — wrapper for [`universal-cookie`](https://www.npmjs.com/package/universal-cookie)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`useFocusTrap`](https://github.com/focus-trap/focus-trap)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/README.md
Expand Up @@ -16,6 +16,7 @@ npm i <b>@vueuse/integrations</b>
<!--FUNCTIONS_LIST_STARTS-->
- [`useAxios`](https://vueuse.org/integrations/useAxios/) — wrapper for [`axios`](https://github.com/axios/axios)
- [`useCookies`](https://vueuse.org/integrations/useCookies/) — wrapper for [`universal-cookie`](https://www.npmjs.com/package/universal-cookie)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`useFocusTrap`](https://github.com/focus-trap/focus-trap)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/index.ts
@@ -1,5 +1,6 @@
export * from './useAxios'
export * from './useCookies'
export * from './useFocusTrap'
export * from './useJwt'
export * from './useNProgress'
export * from './useQRCode'
1 change: 1 addition & 0 deletions packages/integrations/package.json
Expand Up @@ -32,6 +32,7 @@
},
"optionalDependencies": {
"axios": "^0.21.1",
"focus-trap": "^6.3.0",
"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"qrcode": "^1.4.4",
Expand Down
37 changes: 37 additions & 0 deletions packages/integrations/useFocusTrap/demo.vue
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { ref } from 'vue-demi'
import { useFocusTrap } from '.'
const target = ref()
const { hasFocus, activate, deactivate } = useFocusTrap(target)
</script>

<template>
<div class="flex flex-col items-center">
<button @click="activate()">
{{ hasFocus ? 'Focus is trapped within form' : 'Trap focus within form' }}
</button>
<input
type="text"
:placeholder="hasFocus ? 'You can\'t foucs me' : 'You can focus me'"
/>

<div
ref="target"
class="shadow-lg bg-gray-600 bg-opacity-10 dark:(bg-gray-400 bg-opacity-10) rounded max-w-96 mx-auto p-8"
>
<div class="flex flex-row items-center text-6xl text-center justify-center">
<twemoji:face-with-monocle v-if="hasFocus" />
<twemoji:sleeping-face v-else />
</div>
<input type="text" class="!w-12" placeholder="Email">
<input type="text" placeholder="Nickname">
<input placeholder="Password" type="text">
<div class="flex flex-row justify-center">
<button @click="deactivate()">
Free Focus
</button>
</div>
</div>
</div>
</template>
119 changes: 119 additions & 0 deletions packages/integrations/useFocusTrap/index.md
@@ -0,0 +1,119 @@
---
category: '@Integrations'
---

# useFocusTrap

Reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap)

For more info on what options can be passed see [`createOptions`](https://github.com/focus-trap/focus-trap#createfocustrapelement-createoptions) in the `focus-trap` docs.

## Usage

**Basic Usage**

```html
<script setup>
import { ref } from 'vue'
import { useFocusTrap } from '@vueuse/integrations'
const target = ref()
const { hasFocus, activate, deactivate } = useFocusTrap(target)
</script>

<template>
<div>
<button @click="activate()">Activate</button>
<div ref="target">
<span>Has Focus: {{ hasFocus }}</span>
<input type="text" />
<button @click="deactivate()">Deactivate</button>
</div>
</div>
</template>
```

**Automatically Focus**

```html
<script setup>
import { ref } from 'vue'
import { useFocusTrap } from '@vueuse/integrations'
const target = ref()
const { hasFocus, activate, deactivate } = useFocusTrap(target, { immediate: true })
</script>

<template>
<div>
<div ref="target">...</div>
</div>
</template>
```

<!--FOOTER_STARTS-->
## Type Declarations

```typescript
export interface UseFocusTrapOptions extends Options {
/**
* Immediately activate the trap
*/
immediate?: boolean
}
export interface UseFocusTrapReturn {
/**
* Indicates if the focus trap is currently active
*/
hasFocus: Ref<boolean>
/**
* Indicates if the focus trap is currently paused
*/
isPaused: Ref<boolean>
/**
* Activate the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapactivateactivateoptions
* @param opts Activate focus trap options
*/
activate: (opts?: ActivateOptions) => void
/**
* Deactivate the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapdeactivatedeactivateoptions
* @param opts Deactivate focus trap options
*/
deactivate: (opts?: DeactivateOptions) => void
/**
* Pause the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trappause
*/
pause: Fn
/**
* Unpauses the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapunpause
*/
unpause: Fn
}
/**
* Reactive focus-trap
*
* @link https://vueuse.org/integrations/useFocusTrap/
* @param target The target element to trap focus within
* @param options Focus trap options
* @param autoFocus Focus trap automatically when mounted
*/
export declare function useFocusTrap(
target: MaybeElementRef,
options?: UseFocusTrapOptions
): UseFocusTrapReturn
```

## Source

[Source](https://github.com/vueuse/vueuse/blob/main/packages/integrations/useFocusTrap/index.ts) • [Demo](https://github.com/vueuse/vueuse/blob/main/packages/integrations/useFocusTrap/demo.vue) • [Docs](https://github.com/vueuse/vueuse/blob/main/packages/integrations/useFocusTrap/index.md)


<!--FOOTER_ENDS-->
123 changes: 123 additions & 0 deletions packages/integrations/useFocusTrap/index.ts
@@ -0,0 +1,123 @@
import { MaybeElementRef, unrefElement, tryOnUnmounted, Fn } from '@vueuse/core'
import { watch, ref, Ref } from 'vue-demi'
import { ActivateOptions, createFocusTrap, DeactivateOptions, FocusTrap, Options } from 'focus-trap'

export interface UseFocusTrapOptions extends Options {
/**
* Immediately activate the trap
*/
immediate?: boolean
}

export interface UseFocusTrapReturn {
/**
* Indicates if the focus trap is currently active
*/
hasFocus: Ref<boolean>

/**
* Indicates if the focus trap is currently paused
*/
isPaused: Ref<boolean>

/**
* Activate the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapactivateactivateoptions
* @param opts Activate focus trap options
*/
activate: (opts?: ActivateOptions) => void

/**
* Deactivate the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapdeactivatedeactivateoptions
* @param opts Deactivate focus trap options
*/
deactivate: (opts?: DeactivateOptions) => void

/**
* Pause the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trappause
*/
pause: Fn

/**
* Unpauses the focus trap
*
* @link https://github.com/focus-trap/focus-trap#trapunpause
*/
unpause: Fn
}

/**
* Reactive focus-trap
*
* @link https://vueuse.org/useFocusTrap
* @param target The target element to trap focus within
* @param options Focus trap options
* @param autoFocus Focus trap automatically when mounted
*/
export function useFocusTrap(target: MaybeElementRef, options: UseFocusTrapOptions = {}): UseFocusTrapReturn {
let trap: undefined | FocusTrap

const { immediate, ...focusTrapOptions } = options
const hasFocus = ref(false)
const isPaused = ref(false)

const activate = (opts?: ActivateOptions) => trap && trap.activate(opts)
const deactivate = (opts?: DeactivateOptions) => trap && trap.deactivate(opts)

const pause = () => {
if (trap) {
trap.pause()
isPaused.value = true
}
}

const unpause = () => {
if (trap) {
trap.unpause()
isPaused.value = false
}
}

watch(
() => unrefElement(target),
(el: HTMLElement) => {
trap = createFocusTrap(el, {
...focusTrapOptions,
onActivate() {
hasFocus.value = true

// Apply if user provided onActivate option
if (options.onActivate)
options.onActivate()
},
onDeactivate() {
hasFocus.value = false

// Apply if user provided onDeactivate option
if (options.onDeactivate)
options.onDeactivate()
},
})

// Focus if immediate is set to true
if (immediate)
activate()
}, { flush: 'post' })

// Cleanup on unmount
tryOnUnmounted(() => deactivate())

return {
hasFocus,
isPaused,
activate,
deactivate,
pause,
unpause,
}
}
12 changes: 12 additions & 0 deletions yarn.lock
Expand Up @@ -4274,6 +4274,13 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==

focus-trap@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.3.0.tgz#31c08f0b6099705f71f6e0a16d88fbcc4c012586"
integrity sha512-BBzvFfkPg5PqrVVCdQ1YOIVNKGvqG9YNVkiAUQFuDM66N8J9uADhs6mlYKrd30ofDJIzEniBnBKM7GO45iCzKQ==
dependencies:
tabbable "^5.1.5"

follow-redirects@^1.10.0:
version "1.13.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
Expand Down Expand Up @@ -7880,6 +7887,11 @@ symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==

tabbable@^5.1.5:
version "5.1.6"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.1.6.tgz#dd495abe81d5e41e003fbfa70952e20d5e1e1e89"
integrity sha512-KSlGaSX9PbL7FHDTn2dB+zv61prkY8BeGioTsKfeN7dKhw5uz1S4U2iFaWMK4GR8oU+5OFBkFuxbMsaUxVVlrQ==

table@^6.0.4:
version "6.0.7"
resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34"
Expand Down

0 comments on commit d01cc66

Please sign in to comment.