From dbbe2f70f06d909629fbff5797f861b339d2aedf Mon Sep 17 00:00:00 2001
From: Wazeer <58399263+wazeerc@users.noreply.github.com>
Date: Wed, 16 Apr 2025 21:43:45 +0400
Subject: [PATCH] feat: add focus trap functionality to modal component
---
docs/components/modal.md | 29 +++-
.../modal/examples/FwbModalExample.vue | 3 +
.../examples/FwbModalExampleFocusTrap.vue | 15 +++
package-lock.json | 127 ++++++++++++++++--
package.json | 1 +
src/components/FwbModal/FwbModal.vue | 22 ++-
6 files changed, 178 insertions(+), 19 deletions(-)
create mode 100644 docs/components/modal/examples/FwbModalExampleFocusTrap.vue
diff --git a/docs/components/modal.md b/docs/components/modal.md
index b78884d3..f9bc64ad 100644
--- a/docs/components/modal.md
+++ b/docs/components/modal.md
@@ -4,6 +4,7 @@ import FwbModalExampleSize from './modal/examples/FwbModalExampleSize.vue'
import FwbModalExampleEscapable from './modal/examples/FwbModalExampleEscapable.vue'
import FwbModalExamplePersistent from './modal/examples/FwbModalExamplePersistent.vue'
import FwbModalExamplePosition from './modal/examples/FwbModalExamplePosition.vue'
+import FwbModalExampleFocusTrap from './modal/examples/FwbModalExampleFocusTrap.vue'
# Vue Modal - Flowbite
@@ -158,15 +159,33 @@ import { FwbModal } from 'flowbite-vue'
```
+## Focus Trap
+
+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.
+
+
+```vue
+
+
+
+
+
+
+```
+
## API
### Props:
-| Name | Values | Default |
-|--------------|-----------------------------------------------------------|---------|
-| size | `md`,`lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl` | 2xl |
-| notEscapable | `true`, `false` | `false` |
-| persistent | `true`, `false` | `true` |
+| Name | Values | Default |
+|--------------|-----------------------------------------------------------------------------------------------------------------------------------|---------|
+| size | `xs`, `sm`, `md`,`lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl` | `2xl` |
+| position | `top-start`, `top-center`, `top-end`, `center-start`, `center`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end` | `center`|
+| notEscapable | `true`, `false` | `false` |
+| persistent | `true`, `false` | `false` |
+| focusTrap | `true`, `false` | `false` |
### Events:
| Name | Type |
diff --git a/docs/components/modal/examples/FwbModalExample.vue b/docs/components/modal/examples/FwbModalExample.vue
index 2eabfa7b..e2cc95a7 100644
--- a/docs/components/modal/examples/FwbModalExample.vue
+++ b/docs/components/modal/examples/FwbModalExample.vue
@@ -9,6 +9,7 @@
:persistent="persistent"
:size="size"
:position="position"
+ :focus-trap="focusTrap"
@close="closeModal"
>
@@ -56,6 +57,7 @@ interface ModalProps {
persistent?: boolean
triggerText?: string
position?: ModalPosition
+ focusTrap?: boolean
}
withDefaults(defineProps(), {
@@ -64,6 +66,7 @@ withDefaults(defineProps(), {
persistent: false,
triggerText: 'Open Modal',
position: 'center',
+ focusTrap: false,
})
const isShowModal = ref(false)
diff --git a/docs/components/modal/examples/FwbModalExampleFocusTrap.vue b/docs/components/modal/examples/FwbModalExampleFocusTrap.vue
new file mode 100644
index 00000000..29b872c9
--- /dev/null
+++ b/docs/components/modal/examples/FwbModalExampleFocusTrap.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index fbb94380..29ca5d9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/vite": "^4.0.14",
"@vueuse/core": "12.8.2",
+ "@vueuse/integrations": "^13.1.0",
"classnames": "2.5.1",
"floating-vue": "^5.2.2",
"flowbite": "3.1.2",
@@ -19,7 +20,7 @@
"nanoid": "5.1.3",
"postcss-prefix-selector": "^2.1.0",
"tailwind-merge": "3.0.2",
- "tailwindcss": "^4.0.14"
+ "tailwindcss": "^4"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
@@ -3037,15 +3038,13 @@
}
},
"node_modules/@vueuse/integrations": {
- "version": "12.8.2",
- "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz",
- "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==",
- "dev": true,
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.1.0.tgz",
+ "integrity": "sha512-wJ6aANdUs4SOpVabChQK+uLIwxRTUAEmn1DJnflGG7Wq6yaipiRmp6as/Md201FjJnquQt8MecIPbFv8HSBeDA==",
"license": "MIT",
"dependencies": {
- "@vueuse/core": "12.8.2",
- "@vueuse/shared": "12.8.2",
- "vue": "^3.5.13"
+ "@vueuse/core": "13.1.0",
+ "@vueuse/shared": "13.1.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -3062,7 +3061,8 @@
"nprogress": "^0.2",
"qrcode": "^1.5",
"sortablejs": "^1",
- "universal-cookie": "^7"
+ "universal-cookie": "^7",
+ "vue": "^3.5.0"
},
"peerDependenciesMeta": {
"async-validator": {
@@ -3103,6 +3103,44 @@
}
}
},
+ "node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz",
+ "integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "13.1.0",
+ "@vueuse/shared": "13.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz",
+ "integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz",
+ "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
"node_modules/@vueuse/metadata": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
@@ -5472,7 +5510,7 @@
"version": "7.6.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
"integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"tabbable": "^6.2.0"
@@ -9115,7 +9153,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/tailwind-merge": {
@@ -10164,6 +10202,73 @@
"node": ">=12"
}
},
+ "node_modules/vitepress/node_modules/@vueuse/integrations": {
+ "version": "12.8.2",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz",
+ "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "12.8.2",
+ "@vueuse/shared": "12.8.2",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "async-validator": "^4",
+ "axios": "^1",
+ "change-case": "^5",
+ "drauu": "^0.4",
+ "focus-trap": "^7",
+ "fuse.js": "^7",
+ "idb-keyval": "^6",
+ "jwt-decode": "^4",
+ "nprogress": "^0.2",
+ "qrcode": "^1.5",
+ "sortablejs": "^1",
+ "universal-cookie": "^7"
+ },
+ "peerDependenciesMeta": {
+ "async-validator": {
+ "optional": true
+ },
+ "axios": {
+ "optional": true
+ },
+ "change-case": {
+ "optional": true
+ },
+ "drauu": {
+ "optional": true
+ },
+ "focus-trap": {
+ "optional": true
+ },
+ "fuse.js": {
+ "optional": true
+ },
+ "idb-keyval": {
+ "optional": true
+ },
+ "jwt-decode": {
+ "optional": true
+ },
+ "nprogress": {
+ "optional": true
+ },
+ "qrcode": {
+ "optional": true
+ },
+ "sortablejs": {
+ "optional": true
+ },
+ "universal-cookie": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vitepress/node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
diff --git a/package.json b/package.json
index 0fc6740c..e5bb96e4 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/vite": "^4.0.14",
"@vueuse/core": "12.8.2",
+ "@vueuse/integrations": "^13.1.0",
"classnames": "2.5.1",
"floating-vue": "^5.2.2",
"flowbite": "3.1.2",
diff --git a/src/components/FwbModal/FwbModal.vue b/src/components/FwbModal/FwbModal.vue
index deee083c..2c6a3ffa 100644
--- a/src/components/FwbModal/FwbModal.vue
+++ b/src/components/FwbModal/FwbModal.vue
@@ -62,7 +62,8 @@