Comments (5)
You may find the code below useful:
export let focusAfterClosed = null;
let dialogNode;
let lastFocus;
let ignoresFocusChange;
let focused = null;
function modalAction(node) {
focused = document.activeElement;
// mix of https://github.com/KittyGiraudel/focusable-selectors/blob/799829e3b8c329d679b3b414b5dfcfa257a817cf/index.js
// and https://github.com/focus-trap/tabbable/blob/baa8c3044fe0a8fd8c0826f4a3e284872e1467a5/src/index.js#L1-L13
const focusableCandidateSelectors =
'a[href]:not([tabindex^="-"])' +
',area[href]:not([tabindex^="-"])' +
',input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])' +
',input[type="radio"]:not([disabled]):not([tabindex^="-"])' +
',select:not([disabled]):not([tabindex^="-"])' +
',textarea:not([disabled]):not([tabindex^="-"])' +
',button:not([disabled]):not([tabindex^="-"])' +
',iframe:not([tabindex^="-"])' +
',audio[controls]:not([tabindex^="-"])' +
',video[controls]:not([tabindex^="-"])' +
',[contenteditable]:not([tabindex^="-"])' +
',[tabindex]:not([tabindex^="-"])' +
',details>summary:not([tabindex^="-"])' +
',details:not([tabindex^="-"])';
// attemptFocus, focusFirstDescendant, focusLastDesendant, trapFocus logic are
// ported from https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html
const attemptFocus = (element) => {
ignoresFocusChange = true;
try {
element.focus();
} catch {
// ignore
}
ignoresFocusChange = false;
return element === document.activeElement;
};
function focusFirstDescendant(element) {
if (element) {
const descendants = element.querySelectorAll(focusableCandidateSelectors);
for (const el of descendants) {
if (attemptFocus(el)) break;
}
}
}
function focusLastDescendant(element) {
if (element) {
const descendants = element.querySelectorAll(focusableCandidateSelectors);
for (let i = descendants.length - 1; i >= 0; i--) {
if (attemptFocus(descendants[i])) break;
}
}
}
function trapFocus(event) {
if (ignoresFocusChange) return;
if (dialogNode.contains(event.target)) {
lastFocus = event.target;
} else {
focusFirstDescendant(dialogNode);
if (lastFocus === document.activeElement) {
focusLastDescendant(dialogNode);
}
lastFocus = document.activeElement;
}
}
document.addEventListener('focus', trapFocus, true);
focusFirstDescendant(node);
return {
destroy() {
document.removeEventListener('focus', trapFocus, true);
if (focusAfterClosed) focusAfterClosed?.focus();
else focused?.focus();
}
};
}
And then, add this at the top and at the bottom of your Modal:
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div tabindex="0" />
from svelte-put.
@vnphanquang Thanks for taking the time and writing this thoughtful answer.
- I agree that a draggable overlay can be confusing. In most cases, if we have an overlay, we should not be able to click or activate anything underneath it.As a matter of fact, I can't even think of a scenario that someone would want to do that. So my overall thought is if there's an overlay, then there should be a focus trap. But perhaps I'm missing some use cases?
- I agree that headless is the right way to go. In my project, I just switched from
svelte-sonner
to@svelte-put/noti
for that reason, even though I really liked svelte-sonner. But I still thinkfocus trap
, andclickoutside
are not part of the UI, they are part of the functionality underneath. IMHO, a good Headless component should fully cover the UX part, and only leave the cosmetics to the designers. Focustrap, clickoutside, etc. for me fall in the UX area, not UI. I would love to still see these supported in your modal component. - Honestly, I have not used HTML Dialog, as it usually takes a long while before all browsers smoothly support features like this, and usually they leave a lot to desire in the user experience area. But I will take a closer look. Thank you for poitning it out.
from svelte-put.
Thanks very much for the follow-up @akiarostami. Very glad to hear that you are finding noti
useful and we are in agreement with the headless
path.
[...] if there's an overlay, then there should be a focus trap [...]
Yeah, i agree; in that use case it wasn't the best UX, but i didn't have a say in the decision. And honestly it kinda made sense for that particular application at the time.
But the point is still, i don't want to force any opinion upon lib users as i can never predict what this is being used for.
IMHO, a good Headless component should fully cover the UX part, and only leave the cosmetics to the designers.
Same as above, my view is that I should only provide the bare minimum, and an interface that allows features like clickoutside
for focustrap
to be added easily. For further enhancements and best practices, i prefer to have recipes for lib users to copy codes from. It's easy to add code to lib, but often hard to reduce lib code, yeah?
I'm imaging something like this
Click to see code
<!-- MyCustomModal.svelte -->
<script lang="ts">
import {
type ModalInstance,
// optimized, tree-shakable pieces from counter libs (svelte-put/*)
escape,
clickoutside,
focustrap,
lockscroll,
} from '@svelte-put/modal';
import { movable } from '@svelte-put/movable';
// injected prop by lib
export let modal: ModalInstance;
</script>
<div class="modal" use:focustrap use:movable use:clickoutside use:escape use:lockscroll>
<!-- content -->
</div>
[...] it usually takes a long while before all browsers smoothly support features like this [...]
Totally. HTML Dialog, however, is now supported in all major browsers. I think it's probably time we adopt it. Lots of good things it does for free, but i'm not entirely sure how extendable it can be. Experimentation is needed.
Also not quite related but I've been impressed by Melt UI and perhaps there is inspiration worth taking from there.
Your point of view is very valuable to the the redesign of this package. Please do provide feedback about the code sample above. Thank you!
from svelte-put.
You're very kind, @vnphanquang.
Your recipe solution looks very clean.
I've recently seen Melt UI, but I haven't played with it yet. Now that you're recommending it, then I feel I should!
from svelte-put.
@akiarostami Thank you for this. Without doubt modal context should trap focus within it. However, there are a couple of reasons why this was not included in @svelte-put/modal
:
- In a past project, i had to implement an overlay that is draggable and still lets users interact with other elements on the page. In this scenario the core implementation is exactly the same as that of modal (it's not a modal and more like a floating window, i know, forgive me), except for the focus trap part.
@svelte-put/modal
current implementation is mixed between 2 strategies: lib users either extend the predefinedModal
component or create their own custom modal. I kinda regret this; I should have pursued the "headless" path more aggressively, as I've recently done with @svelte-put/tooltip & @svelte-put/noti, where lib only provides the component stack logics and leave the UI completely out. My plan is to deprecate the predefined components and simplify the public interface in the next major release. Once that is done, the UI, including focus trap, is up to lib users; and things like clickoutside and movable should be easily added on demand as svelte actions.- recently, HTML dialog has gone more and more mainstream. There is in fact a showModal method on
HTMLDialogElement
that natively does focus trap. I'm experimenting with ways to wrap arounddialog
and hopefully can take advantage of those native supports.
I'm curious to hear more of your thought my reasoning above. In the meantime, would it be helpful if I introduce a svelte-put/focus-trap
svelte action that perhaps will help you with the reported issue here?
from svelte-put.
Related Issues (20)
- [code] - Implement Code component with syntax highlighting
- [docs] Reorganize components & route structures
- Setup renovate for dependency PR and update
- All about Testing
- Notification: pause, resume, and $progress
- @svelte-put/lockscroll: show error when use:lockscroll opn svelte:document HOT 1
- movables do not behave as expected on mobile / with touch input HOT 2
- Change clickoutside node type to Element or Node to allow usage on SVG elements
- toc page mentions anchor config in passing but not explicitly HOT 2
- shortcut: get keyboard event / actual target
- Ignoring Hotkeys not working HOT 2
- Missing exports condition HOT 6
- Modal.svelte contains svelte a11y warning HOT 3
- Generated table of contents does not reflect on header order if it is a number HOT 8
- scrollock not working on ipadOs 17.2 same for ios HOT 2
- Movable - Cannot limit delta for only 1 axis. HOT 1
- Revamp Docs Site HOT 1
- [QR] margin will shift logo HOT 1
- Add qr error correction option HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from svelte-put.