trap-focus-svelte
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -1,12 +0,8 @@ | ||
interface TrapOptions { | ||
/** enables or disables wrap */ | ||
active?: boolean; | ||
/** wrapper element */ | ||
wrap?: HTMLElement; | ||
} | ||
/** Traps focus within a wrapper element */ | ||
declare function trapFocus(node: HTMLElement, options?: TrapOptions): { | ||
update(options: TrapOptions): void; | ||
declare function trapFocus(wrap: HTMLElement, active?: boolean): { | ||
/** Enables / disables trap */ | ||
update(active: boolean): void; | ||
/** Destroys trap and removes event listeners */ | ||
destroy(): void; | ||
}; | ||
export { trapFocus }; |
@@ -1,1 +0,1 @@ | ||
import{listen as t}from"svelte/internal";let e=[],o=!1;function c(c,n={active:!0}){var u,l;let{wrap:r=c,active:a}=n;const s=[...r.querySelectorAll("*")].filter(t=>t.tabIndex>=0),i=null!=(u=s.at(0))?u:r,f=null!=(l=s.at(-1))?l:r,d=document.activeElement;e.push(r),i.focus();const m=(t=r)=>e.at(-1).contains(t),v=t(i,"blur",()=>{a&&m()&&o&&f.focus()}),p=t(f,"blur",()=>{a&&m()&&!o&&i.focus()}),y=t(document,"focusin",t=>{a&&!m(t.target)&&(o?f:i).focus()});return{update(t){a=t.active},destroy(){y(),v(),p(),e=e.filter(t=>t!==r),d.focus()}}}t(document,"keydown",t=>{o=t.shiftKey&&"Tab"===t.key});export{c as trapFocus}; | ||
import{listen as t}from"svelte/internal";let e=[],o=0;function n(n,c=1){function u(){const t=[...n.querySelectorAll("*")].filter((t=>t.tabIndex>=0));return[t.at(0)??n,t.at(-1)??n]}let f;function r(){e.push(n),f=document.activeElement,u().at(0).focus()}function s(){e=e.filter((t=>t!=n)),f.focus()}c&&r();const i=t=>e.at(-1)?.contains(t),a=t(n,"focusout",(t=>{if(i(n)){const[e,n]=u();t.target==e&&o?n.focus():t.target!=n||o||e.focus()}})),l=t(document,"focusin",(t=>{if(i(n)&&!i(t.target)){const[t,e]=u();(o?e:t).focus()}}));return{update(t){t?r():s()},destroy(){l(),a(),s()}}}t(document,"keydown",(t=>o=t.shiftKey&&"Tab"==t.key));export{n as trapFocus}; |
98
index.ts
import { listen } from 'svelte/internal' | ||
interface TrapOptions { | ||
/** enables or disables wrap */ | ||
active?: boolean | ||
/** wrapper element */ | ||
wrap?: HTMLElement | ||
} | ||
let stack: HTMLElement[] = [] | ||
@@ -15,45 +8,54 @@ | ||
listen(document, 'keydown', (e: KeyboardEvent) => { | ||
shiftTab = e.shiftKey && e.key === 'Tab' | ||
}) | ||
listen(document, 'keydown', (e: KeyboardEvent) => (shiftTab = e.shiftKey && e.key == 'Tab')) | ||
/** Traps focus within a wrapper element */ | ||
function trapFocus(node: HTMLElement, options: TrapOptions = { active: true }) { | ||
let { wrap = node, active } = options | ||
function trapFocus(wrap: HTMLElement, active = true) { | ||
// return the first and last focusable children | ||
function getFirstAndLastFocusable() { | ||
const els = [...wrap.querySelectorAll('*')].filter( | ||
(element: HTMLElement) => element.tabIndex >= 0 | ||
) | ||
return [els.at(0) ?? wrap, els.at(-1) ?? wrap] as HTMLElement[] | ||
} | ||
const focusableEls = [...wrap.querySelectorAll('*')].filter( | ||
(element: HTMLElement) => element.tabIndex >= 0 | ||
) as HTMLElement[] | ||
// store document.activeElement to restore focus when untrapped | ||
let lastActiveElement: HTMLElement | ||
const firstFocusableEl = focusableEls.at(0) ?? wrap | ||
const lastFocusableEl = focusableEls.at(-1) ?? wrap | ||
const lastActiveElement = document.activeElement as HTMLElement | ||
/** activates trap (adds to stack) and focuses inside */ | ||
function addToStack() { | ||
stack.push(wrap) | ||
lastActiveElement = document.activeElement as HTMLElement | ||
getFirstAndLastFocusable().at(0).focus() | ||
} | ||
/** deactivates trap (removes from stack) and restores focus to lastActiveElement */ | ||
function removeFromStack() { | ||
stack = stack.filter((el) => el != wrap) | ||
lastActiveElement.focus() | ||
} | ||
// add to stack | ||
stack.push(wrap) | ||
// add to stack if active | ||
if (active) { | ||
addToStack() | ||
} | ||
// set initial focus on first focusable el | ||
firstFocusableEl.focus() | ||
/** true if element is in the trap most recently added to stack */ | ||
const inCurrentTrap = (el: HTMLElement) => stack.at(-1)?.contains(el) | ||
/** true if element is in the last trap added to stack */ | ||
const inCurrentTrap = (el: HTMLElement = wrap) => stack.at(-1).contains(el) | ||
// use blur listeners to redirect focus before it leaves container\ | ||
/** focus last element if focus leaves first element with shift */ | ||
const firstElBlurListener = listen(firstFocusableEl, 'blur', () => { | ||
if (active && inCurrentTrap()) { | ||
shiftTab && lastFocusableEl.focus() | ||
/** loop focus if leaving first of last focusable element in wrap */ | ||
const focusOutListener = listen(wrap, 'focusout', (e: FocusEvent) => { | ||
if (inCurrentTrap(wrap)) { | ||
const [firstFocusableEl, lastFocusableEl] = getFirstAndLastFocusable() | ||
if (e.target == firstFocusableEl && shiftTab) { | ||
lastFocusableEl.focus() | ||
} else if (e.target == lastFocusableEl && !shiftTab) { | ||
firstFocusableEl.focus() | ||
} | ||
} | ||
}) | ||
/** focus first element if focus leaves last element without shift */ | ||
const lastElBlurListener = listen(lastFocusableEl, 'blur', () => { | ||
if (active && inCurrentTrap()) { | ||
!shiftTab && firstFocusableEl.focus() | ||
} | ||
}) | ||
/** listener for focus event, moves focus to container if away */ | ||
/** moves focus back to wrap if something outside the wrap is focused */ | ||
const focusListener = listen(document, 'focusin', (e: FocusEvent) => { | ||
if (active && !inCurrentTrap(e.target as HTMLElement)) { | ||
let focusEl = shiftTab ? lastFocusableEl : firstFocusableEl | ||
if (inCurrentTrap(wrap) && !inCurrentTrap(e.target as HTMLElement)) { | ||
const [first, last] = getFirstAndLastFocusable() | ||
let focusEl = shiftTab ? last : first | ||
focusEl.focus() | ||
@@ -64,13 +66,15 @@ } | ||
return { | ||
update(options: TrapOptions) { | ||
active = options.active | ||
/** Enables / disables trap */ | ||
update(active: boolean) { | ||
if (active) { | ||
addToStack() | ||
} else { | ||
removeFromStack() | ||
} | ||
}, | ||
/** Destroys trap and removes event listeners */ | ||
destroy() { | ||
// remove listeners | ||
focusListener() | ||
firstElBlurListener() | ||
lastElBlurListener() | ||
// remove from stack & focus previously focused element | ||
stack = stack.filter((el) => el !== wrap) | ||
lastActiveElement.focus() | ||
focusOutListener() | ||
removeFromStack() | ||
}, | ||
@@ -77,0 +81,0 @@ } |
134
package.json
{ | ||
"name": "trap-focus-svelte", | ||
"version": "0.1.1", | ||
"license": "MIT", | ||
"description": "Small 0.4kB focus trap that supports stacking, toggling, and custom scope", | ||
"keywords": [ | ||
"focus", | ||
"focus trap", | ||
"focus lock", | ||
"svelte" | ||
], | ||
"author": "Hank Dollman <hank@henrygd.me> (https://henrygd.me)", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/henrygd/trap-focus-svelte.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/henrygd/trap-focus-svelte/issues" | ||
}, | ||
"homepage": "https://trap-focus-svelte.henrygd.me", | ||
"type": "module", | ||
"source": "index.ts", | ||
"exports": { | ||
".": { | ||
"import": "./dist/trap-focus-svelte.svelte.js", | ||
"require": "./dist/trap-focus-svelte.cjs", | ||
"default": "./dist/trap-focus-svelte.svelte.js" | ||
}, | ||
"./svelte": "./dist/trap-focus-svelte.svelte.js", | ||
"./vanilla": "./dist/trap-focus-svelte.mjs" | ||
}, | ||
"svelte": "./dist/trap-focus-svelte.svelte.js", | ||
"types": "dist/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
"vanilla": [ | ||
"dist/index.d.ts" | ||
], | ||
"svelte": [ | ||
"dist/index.d.ts" | ||
] | ||
} | ||
}, | ||
"scripts": { | ||
"build": "run-p build-cjs build-module build-svelte && sed -i 's/Promise.resolve();//' dist/*", | ||
"build-cjs": "microbundle -i index.ts -o ./dist/trap-focus-svelte.cjs.js --no-pkg-main -f cjs --sourcemap false", | ||
"build-module": "microbundle -i index.ts -o ./dist/trap-focus-svelte.mjs --no-pkg-main -f modern --sourcemap false", | ||
"build-svelte": "microbundle -i index.ts -o ./dist/trap-focus-svelte.svelte.js --no-pkg-main -f modern --external svelte --sourcemap false", | ||
"dev": "vite demo", | ||
"demo-build": "vite build demo", | ||
"preview": "vite demo preview", | ||
"check": "svelte-check --tsconfig ./tsconfig.json" | ||
}, | ||
"devDependencies": { | ||
"@sveltejs/vite-plugin-svelte": "^2.0.2", | ||
"@tsconfig/svelte": "^3.0.0", | ||
"hide-show-scroll": "^2.0.0", | ||
"microbundle": "^0.15.1", | ||
"npm-run-all": "^4.1.5", | ||
"svelte": "^3.55.1", | ||
"svelte-check": "^2.10.3", | ||
"tslib": "^2.5.0", | ||
"typescript": "^4.9.3", | ||
"vite": "^4.1.0" | ||
} | ||
} | ||
"name": "trap-focus-svelte", | ||
"version": "0.2.0", | ||
"license": "MIT", | ||
"description": "Small 0.4kB focus trap that supports stacking, toggling, and dynamic content. Compatible with any framework.", | ||
"keywords": [ | ||
"focus", | ||
"focus trap", | ||
"focus lock", | ||
"svelte" | ||
], | ||
"author": "Hank Dollman <hank@henrygd.me> (https://henrygd.me)", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/henrygd/trap-focus-svelte.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/henrygd/trap-focus-svelte/issues" | ||
}, | ||
"homepage": "https://trap-focus-svelte.henrygd.me", | ||
"type": "module", | ||
"source": "index.ts", | ||
"exports": { | ||
".": { | ||
"import": "./dist/trap-focus-svelte.svelte.js", | ||
"require": "./dist/trap-focus-svelte.cjs", | ||
"default": "./dist/trap-focus-svelte.svelte.js" | ||
}, | ||
"./svelte": "./dist/trap-focus-svelte.svelte.js", | ||
"./vanilla": "./dist/trap-focus-svelte.mjs" | ||
}, | ||
"svelte": "./dist/trap-focus-svelte.svelte.js", | ||
"types": "dist/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
"vanilla": [ | ||
"dist/index.d.ts" | ||
], | ||
"svelte": [ | ||
"dist/index.d.ts" | ||
] | ||
} | ||
}, | ||
"devDependencies": { | ||
"@rollup/plugin-node-resolve": "^15.0.1", | ||
"@rollup/plugin-terser": "^0.4.0", | ||
"@rollup/plugin-typescript": "^11.0.0", | ||
"@sveltejs/vite-plugin-svelte": "^2.0.2", | ||
"@tsconfig/svelte": "^3.0.0", | ||
"hide-show-scroll": "^2.0.0", | ||
"npm-run-all": "^4.1.5", | ||
"rollup": "^3.14.0", | ||
"rollup-plugin-modify": "^3.0.0", | ||
"rollup-plugin-size": "^0.3.1", | ||
"svelte": "^3.55.1", | ||
"svelte-check": "^2.10.3", | ||
"tslib": "^2.5.0", | ||
"typescript": "^4.9.3", | ||
"vite": "^4.1.0" | ||
}, | ||
"scripts": { | ||
"build": "run-p rollup-build generate-types", | ||
"rollup-build": "rollup -c", | ||
"generate-types": "tsc -p . --declaration --emitDeclarationOnly --outDir dist", | ||
"dev": "vite demo", | ||
"demo-build": "vite build demo", | ||
"preview": "vite demo preview", | ||
"check": "svelte-check --tsconfig ./tsconfig.json" | ||
} | ||
} |
@@ -0,5 +1,13 @@ | ||
[npm-image]: https://flat.badgen.net/npm/v/trap-focus-svelte?color=blue | ||
[npm-url]: https://www.npmjs.com/package/trap-focus-svelte | ||
[size-image]: https://flat.badgen.net/badgesize/gzip/henrygd/trap-focus-svelte/main/dist/trap-focus-svelte.svelte.js?color=green | ||
[license-image]: https://flat.badgen.net/github/license/henrygd/trap-focus-svelte?color=purple | ||
[license-url]: /license | ||
# trap-focus-svelte | ||
Small 0.4kB focus trap that supports stacking, toggling, and custom scope. Designed for Svelte but usable with vanilla js or any framework. | ||
[![npm][npm-image]][npm-url] ![File Size][size-image] [![MIT license][license-image]][license-url] | ||
Small 0.4kB focus trap that supports stacking, toggling, and dynamic content. Designed for Svelte but compatible with plain JavaScript or any framework. | ||
Demo: https://trap-focus-svelte.henrygd.me | ||
@@ -17,12 +25,76 @@ | ||
## Options | ||
## Usage with Svelte | ||
ADD OPTIONS INFO | ||
Add directly to an element as an action. | ||
## Usage with Svelte | ||
If the element is removed, the trap and event listeners are destroyed automatically. | ||
ADD USAGE INFO | ||
```html | ||
<script> | ||
import { trapFocus } from 'trap-focus-svelte' | ||
</script> | ||
## Usage with vanilla / other frameworks | ||
<div use:trapFocus> | ||
<button>Inside trap</button> | ||
<button>Inside trap</button> | ||
</div> | ||
<button>Outside trap</button> | ||
``` | ||
ADD USAGE INFO | ||
You can also toggle the trap on they fly: | ||
<!-- prettier-ignore-start --> | ||
```html | ||
<script> | ||
import { trapFocus } from 'trap-focus-svelte' | ||
let active = true | ||
const toggleTrap = () => (active = !active) | ||
</script> | ||
<div use:trapFocus={active}> | ||
<button on:click={toggleTrap}>Toggle trap</button> | ||
<button>Inside trap</button> | ||
</div> | ||
<button>Outside trap</button> | ||
``` | ||
<!-- prettier-ignore-end --> | ||
## Usage with vanilla JavaScript or other frameworks | ||
Import from `trap-focus-svelte/vanilla` | ||
For an example of the demo site made without Svelte (with TypeScript / Vite) see [this StackBlitz](https://stackblitz.com/edit/vitejs-vite-sesxte?file=src/main.ts). | ||
```html | ||
<div id="buttons"> | ||
<button>Inside trap</button> | ||
<button>Inside trap</button> | ||
</div> | ||
<button>Outside trap</button> | ||
``` | ||
```js | ||
import { trapFocus } from 'trap-focus-svelte/vanilla' | ||
const div = document.getElementById('buttons') | ||
// create trap (pass false as a second argument to start disabled) | ||
const buttonTrap = trapFocus(buttons) | ||
// toggle trap | ||
buttonTrap.update(false) | ||
// destory trap | ||
buttonTrap.destroy() | ||
``` | ||
## Notes | ||
If you have multiple traps active at the same time, the focus will be within the latest trap created or activated. | ||
When that trap is destroyed or deactivated, the focus will work backwards down the chain, eventually restoring focus to the [`document.activeElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement) at the time of the oldest trap's activation. | ||
## License | ||
MIT |
{ | ||
"include": ["index.ts"], | ||
"compilerOptions": { | ||
@@ -3,0 +4,0 @@ "target": "esnext", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
12564
12
156
100
15
1