-
Notifications
You must be signed in to change notification settings - Fork 130
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
Can the History API be utilized in a11y-dialog? #363
Comments
Hey Piotr! I assume it’s possible to do, but I have never tried. It’s an interesting use case, and I’ve heard people praising URL-based dialogs—I've just never encountered a case where I needed it. If someone manages to make it work, I’d love to have a demo + section in the documentation for that. |
I've given a stab at it in that fork of the demo CodeSandbox: https://codesandbox.io/s/a11y-dialog-forked-v99mnd?file=/src/index.js Unfortunately, this required me to override the To save a trip to CodeSandbox, here's what I ended up with: import A11yDialog from 'a11y-dialog'
var dialogEl = document.getElementById('my-dialog')
var mainEl = document.querySelector('main')
// TRACKING A FEW THINGS
// Re-opening the dialog on `forward` means
// we need some reference to it.
// The history API provides a `state` to store
// things in, but we can't store the A11yDialog object
// So we need a little place to store which A11yDialog
// object corresponds to which dialog
// without worrying about cleaning up when dialogs get destroyed
const dialogs = new WeakMap()
// The other way around, when using `back`,
// we'll need to close the latest opened dialog
// so we need a little structure to track that as well
// Using an Array opens the possibility
// of handling nested dialogs properly, even if it's a bad plan.
const openDialogs = []
// We need a little boolean to know whether we're currently
// handling a `popstate`, as we wouldn't want to `pushstate`
// again when re-opening the popup going forward.
// That'd lead to pushing the "open" state again and again
// when alternating back & forward clicks
let withinPopState
window.addEventListener('popstate', function (event) {
withinPopState = true
requestAnimationFrame(() => {
withinPopState = false
})
})
// NOW TO THAT DIALOG
// That's an unfortunate but necessary bit:
// we need to take over how the dialog gets closed,
// in order to update the URL through the history API.
// `replaceState` wouldn't do here as closing the dialoge
// should actually move to the previous state.
// Needs to happen BEFORE creating the dialog
// as the bound function is directly used as a listener
const originalHide = A11yDialog.prototype.hide
A11yDialog.prototype.hide = function (event) {
if (withinPopState) {
// We only want to hide while handling a `popstate` event
originalHide.apply(this, arguments)
} else {
// The rest of the time, we want to trigger that `popstate` event
// by going back in the history
window.history.back()
}
}
var dialog = new A11yDialog(dialogEl, mainEl)
dialogs.set(dialogEl, dialog)
dialog.on('show', function (dialogEl) {
const dialog = dialogs.get(dialogEl)
// Store that the dialog is the new open dialog
openDialogs.push(dialog)
// TODO: Use the `destroy` and `hide` events
// to make sure `openDialogs` doesn't leak
// But for the purpose of history nav.
// that's parallel
})
// AND THE HISTORY HANDLING
// When showing the dialog, we'll update the URL
// and store the ID of the dialog
dialog.on('show', function (dialogEl) {
if (!withinPopState) {
window.history.pushState(
{
dialogId: dialogEl.id,
},
'', // This is unused
// The URL would likely come from a data-attribute, `href` or falling back to the ID of the dialog
'/something'
)
// TODO: We'd likely want to update the page title as well
// and restore it when closing the modal.
// Using `replaceState` here before pushing could let us
// save the current one and access it during the `popstate`
// when restoring the page.
// The title for the modal would need to be pulled from somewhere
// like a `data-page-title` attribute
}
})
// `popstate` will be our trigger for closing the dialog and opening it back
// Especially closing, for which we overrod `A11yDialog.prototype.hide` at the start
window.addEventListener('popstate', function (event) {
// Other parts may be using the history API
// So those won't always be present.
// Not having them means we're back to before any dialog was open
// and we can skip to closing the dialogs
if (event.state && event.state.dialogId) {
// TODO: Handle nested dialogs here
// They would have a `dialogId` in their state
// but it wouldn't match that of the last dialog in `openDialogs`
// If we have a stored state, it means we're going forward
// back to the page where the dialog was open.
// We need to recover our dialog and open it
const dialogEl = document.getElementById(event.state.dialogId)
const dialog = dialogs.get(dialogEl)
if (dialog) {
dialog.show()
}
return
}
const openDialog = openDialogs.pop()
if (openDialog) {
openDialog.hide()
}
}) |
intriguing.
shouldn't |
IIRC vanilla HTML forms using native post method are excluded from browser history. What about "stepping back" to a dialog that contained a form/FormData already sent/posted to the server via JS? |
@WebMechanic For the title, it'd sure be possible to pick the Regarding the behaviour with form submissions, I'm not sure I understand what you mean by "native post [...] are excluded from browsing history". I recall browsers asking for confirmation of form resubmission when pressing back after submitting a POST form that didn't redirect, with another press of back showing the form again? In any case, I'd say that's the responsibility of whatever submits the form via JS to not break that, regardless of whether the form was shown inside a dialog or not. Your mention of form submission through JS makes me think it may be worth checking if browsers keep whatever is in the history state after letting them submit without JS and pressing back to get back to the form. If that's the case, it'd probably be worth looking into |
@romaricpascal let me first make clear, that I believe easy access to history.state and dialog states could be a nice UX, however I understand some might want to have all sorts of options if, when and how to update the window/document title. Anyhow, an updated title could make a good bookmark label. I for one never had the urge to bookmark "a dialog state" in the last 25 years, let alone one with an open (partially filled out) form in it, 'cos it's not really a thing. It might become an anti-feature Some "remember this dialog" checkbox inside any history-aware dialog might be useful pattern to allow users to opt-in into this behaviour. On the whole, perhaps something any of this would be best implemented as a kind of "plugin" that is executed during the dialog's lifecycle events to also make it clear to the developer this "goodie" can end up in writing lots of additional app and server code to make it work smoothly and become a solid UX.
I believe that is what I had in mind - or the other way around :D I'm not sure how page/tab lifecycle could mess with history state, length, data and what events would be required to listen to after a tab was suspended, closed, re-opened etc. with a dialog currently on display.
|
@romaricpascal I know it’s been forever, but I wanted to let you know that v8.1.0 makes all events cancellable. |
Would it be possible for this library to get history api support, like one described there?
https://github.com/jamesnuanez/history-api-with-modals-demo
I think it would be especially useful on mobile devices where users are used to closing modals with back button.
I played around a little with implementing it using a11y dialog events but didn't sucedeed.
The text was updated successfully, but these errors were encountered: