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

feat(useFocusTrap): new function #433

Merged
merged 8 commits into from Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -7,7 +7,7 @@ Collection of essential Vue Composition Utilities
<a href="https://www.npmjs.com/package/@vueuse/core" target="__blank"><img src="https://img.shields.io/npm/v/@vueuse/core?color=a1b858&label=" alt="NPM version"></a>
<a href="https://www.npmjs.com/package/@vueuse/core" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@vueuse/core?color=50a36f&label="></a>
<a href="https://vueuse.org" target="__blank"><img src="https://img.shields.io/static/v1?label=&message=docs%20%26%20demos&color=1e8a7a" alt="Docs & Demos"></a>
<img alt="Function Count" src="https://img.shields.io/badge/-112%20functions-13708a">
<img alt="Function Count" src="https://img.shields.io/badge/-113%20functions-13708a">
<br>
<a href="https://github.com/vueuse/vueuse" target="__blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/vueuse/vueuse?style=social"></a>
</p>
Expand Down
7 changes: 7 additions & 0 deletions indexes.json
Expand Up @@ -816,6 +816,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()">
Trap Focus Within Form
</button>
wheatjs marked this conversation as resolved.
Show resolved Hide resolved
<input
type="text"
:placeholder="hasFocus ? 'You can\'t foucs me' : 'You can focus me'"
wheatjs marked this conversation as resolved.
Show resolved Hide resolved
/>

<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