Skip to content

Commit 31ffaaf

Browse files
authored
Merge pull request #367 from wazeerc/feature/modal-focustrap
Add focus trap functionality to modal component
2 parents ec2bbd3 + dbbe2f7 commit 31ffaaf

File tree

6 files changed

+178
-19
lines changed

6 files changed

+178
-19
lines changed

docs/components/modal.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import FwbModalExampleSize from './modal/examples/FwbModalExampleSize.vue'
44
import FwbModalExampleEscapable from './modal/examples/FwbModalExampleEscapable.vue'
55
import FwbModalExamplePersistent from './modal/examples/FwbModalExamplePersistent.vue'
66
import FwbModalExamplePosition from './modal/examples/FwbModalExamplePosition.vue'
7+
import FwbModalExampleFocusTrap from './modal/examples/FwbModalExampleFocusTrap.vue'
78
</script>
89
# Vue Modal - Flowbite
910

@@ -158,15 +159,33 @@ import { FwbModal } from 'flowbite-vue'
158159
</script>
159160
```
160161

162+
## Focus Trap
163+
164+
You can enable focus trapping by setting the `focus-trap` prop to `true`. This keeps the focus within the modal, preventing users from tabbing to elements outside of it, which improves accessibility.
165+
166+
<fwb-modal-example-focus-trap />
167+
```vue
168+
<template>
169+
<fwb-modal />
170+
<fwb-modal focus-trap />
171+
</template>
172+
173+
<script setup>
174+
import { FwbModal } from 'flowbite-vue'
175+
</script>
176+
```
177+
161178
## API
162179

163180
### Props:
164181

165-
| Name | Values | Default |
166-
|--------------|-----------------------------------------------------------|---------|
167-
| size | `md`,`lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl` | 2xl |
168-
| notEscapable | `true`, `false` | `false` |
169-
| persistent | `true`, `false` | `true` |
182+
| Name | Values | Default |
183+
|--------------|-----------------------------------------------------------------------------------------------------------------------------------|---------|
184+
| size | `xs`, `sm`, `md`,`lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl` | `2xl` |
185+
| position | `top-start`, `top-center`, `top-end`, `center-start`, `center`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end` | `center`|
186+
| notEscapable | `true`, `false` | `false` |
187+
| persistent | `true`, `false` | `false` |
188+
| focusTrap | `true`, `false` | `false` |
170189

171190
### Events:
172191
| Name | Type |

docs/components/modal/examples/FwbModalExample.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
:persistent="persistent"
1010
:size="size"
1111
:position="position"
12+
:focus-trap="focusTrap"
1213
@close="closeModal"
1314
>
1415
<template #header>
@@ -56,6 +57,7 @@ interface ModalProps {
5657
persistent?: boolean
5758
triggerText?: string
5859
position?: ModalPosition
60+
focusTrap?: boolean
5961
}
6062
6163
withDefaults(defineProps<ModalProps>(), {
@@ -64,6 +66,7 @@ withDefaults(defineProps<ModalProps>(), {
6466
persistent: false,
6567
triggerText: 'Open Modal',
6668
position: 'center',
69+
focusTrap: false,
6770
})
6871
6972
const isShowModal = ref(false)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div class="vp-raw flex justify-start space-x-2">
3+
<fwb-modal-example
4+
trigger-text="Without Focus Trap"
5+
/>
6+
<fwb-modal-example
7+
focus-trap
8+
trigger-text="With Focus Trap"
9+
/>
10+
</div>
11+
</template>
12+
13+
<script lang="ts" setup>
14+
import FwbModalExample from './FwbModalExample.vue'
15+
</script>

package-lock.json

Lines changed: 116 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@tailwindcss/postcss": "^4.0.14",
7070
"@tailwindcss/vite": "^4.0.14",
7171
"@vueuse/core": "12.8.2",
72+
"@vueuse/integrations": "^13.1.0",
7273
"classnames": "2.5.1",
7374
"floating-vue": "^5.2.2",
7475
"flowbite": "3.1.2",

src/components/FwbModal/FwbModal.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
</template>
6363

6464
<script lang="ts" setup>
65-
import { onMounted, ref, type Ref } from 'vue'
65+
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
66+
import { nextTick, onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
6667
6768
import type { ModalPosition, ModalSize } from './types'
6869
@@ -71,13 +72,15 @@ interface ModalProps {
7172
persistent?: boolean
7273
size?: ModalSize
7374
position?: ModalPosition
75+
focusTrap?: boolean
7476
}
7577
7678
const props = withDefaults(defineProps<ModalProps>(), {
7779
notEscapable: false,
7880
persistent: false,
7981
size: '2xl',
8082
position: 'center',
83+
focusTrap: false,
8184
})
8285
8386
const emit = defineEmits(['close', 'click:outside'])
@@ -120,10 +123,23 @@ function clickOutside () {
120123
function closeWithEsc () {
121124
if (!props.notEscapable && !props.persistent) closeModal()
122125
}
126+
123127
const modalRef: Ref<HTMLElement | null> = ref(null)
124-
onMounted(() => {
128+
const { activate, deactivate } = useFocusTrap(modalRef, {
129+
immediate: false,
130+
initialFocus: () => modalRef.value?.querySelector('button[aria-label="close"]') || modalRef.value,
131+
})
132+
133+
onMounted(async () => {
125134
if (modalRef.value) {
126-
modalRef.value.focus()
135+
if (props.focusTrap) {
136+
await nextTick()
137+
activate()
138+
}
127139
}
128140
})
141+
142+
onBeforeUnmount(() => {
143+
deactivate()
144+
})
129145
</script>

0 commit comments

Comments
 (0)