mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 21:36:36 +00:00
Simplify dialog navigation to fix back button (#23220)
* Simplify dialog navigation to fix back btn * add back comment * manage dialog stack in the manager instead of history * handle dialogs that refuse to close
This commit is contained in:
parent
84157c8ea5
commit
ed625d4e0b
@ -1,4 +1,3 @@
|
||||
import { historyPromise } from "../state/url-sync-mixin";
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
|
||||
@ -17,11 +16,6 @@ export interface NavigateOptions {
|
||||
export const navigate = (path: string, options?: NavigateOptions) => {
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (historyPromise) {
|
||||
historyPromise.then(() => navigate(path, options));
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
|
@ -37,10 +37,12 @@ export interface DialogClosedParams {
|
||||
}
|
||||
|
||||
export interface DialogState {
|
||||
dialog: string;
|
||||
open: boolean;
|
||||
oldState: null | DialogState;
|
||||
dialogParams?: unknown;
|
||||
element: HTMLElement & ProvideHassElement;
|
||||
root: ShadowRoot | HTMLElement;
|
||||
dialogTag: string;
|
||||
dialogParams: unknown;
|
||||
dialogImport?: () => Promise<unknown>;
|
||||
addHistory?: boolean;
|
||||
}
|
||||
|
||||
interface LoadedDialogInfo {
|
||||
@ -53,6 +55,7 @@ interface LoadedDialogsDict {
|
||||
}
|
||||
|
||||
const LOADED: LoadedDialogsDict = {};
|
||||
const OPEN_DIALOG_STACK: DialogState[] = [];
|
||||
export const FOCUS_TARGET = Symbol.for("HA focus target");
|
||||
|
||||
export const showDialog = async (
|
||||
@ -77,52 +80,42 @@ export const showDialog = async (
|
||||
element: dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag) as HassDialog;
|
||||
element.provideHass(dialogEl);
|
||||
dialogEl.addEventListener("dialog-closed", _handleClosed);
|
||||
dialogEl.addEventListener("dialog-closed", _handleClosedFocus);
|
||||
return dialogEl;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Get the focus targets after the dialog closes, but keep the original if dialog is being replaced
|
||||
if (mainWindow.history.state?.replaced) {
|
||||
LOADED[dialogTag].closedFocusTargets =
|
||||
LOADED[mainWindow.history.state.dialog].closedFocusTargets;
|
||||
delete LOADED[mainWindow.history.state.dialog].closedFocusTargets;
|
||||
} else {
|
||||
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
|
||||
deepActiveElement(),
|
||||
FOCUS_TARGET
|
||||
);
|
||||
}
|
||||
// Get the focus targets after the dialog closes
|
||||
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
|
||||
deepActiveElement(),
|
||||
FOCUS_TARGET
|
||||
);
|
||||
|
||||
const { state } = mainWindow.history;
|
||||
// if the same dialog is already open, don't push state
|
||||
if (addHistory) {
|
||||
mainWindow.history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
mainWindow.history.state?.open &&
|
||||
mainWindow.history.state?.dialog !== dialogTag
|
||||
? mainWindow.history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err: any) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
""
|
||||
);
|
||||
OPEN_DIALOG_STACK.push({
|
||||
element,
|
||||
root,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
addHistory,
|
||||
});
|
||||
const newState = { dialog: dialogTag };
|
||||
if (state?.dialog) {
|
||||
// if the dialog is already open, replace the name
|
||||
mainWindow.history.replaceState(newState, "");
|
||||
} else {
|
||||
// if the dialog is not open, push a new state so back() will close the dialog
|
||||
mainWindow.history.replaceState({ ...state, opensDialog: true }, "");
|
||||
mainWindow.history.pushState(newState, "");
|
||||
}
|
||||
}
|
||||
|
||||
const dialogElement = await LOADED[dialogTag].element;
|
||||
dialogElement.addEventListener("dialog-closed", _handleClosedFocus);
|
||||
|
||||
// Append it again so it's the last element in the root,
|
||||
// so it's guaranteed to be on top of the other elements
|
||||
@ -132,12 +125,23 @@ export const showDialog = async (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const replaceDialog = (dialogElement: HassDialog) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, replaced: true },
|
||||
""
|
||||
export const showDialogFromHistory = async (dialogTag: string) => {
|
||||
const dialogState = OPEN_DIALOG_STACK.find(
|
||||
(state) => state.dialogTag === dialogTag
|
||||
);
|
||||
dialogElement.removeEventListener("dialog-closed", _handleClosedFocus);
|
||||
if (dialogState) {
|
||||
showDialog(
|
||||
dialogState.element,
|
||||
dialogState.root,
|
||||
dialogTag,
|
||||
dialogState.dialogParams,
|
||||
dialogState.dialogImport,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
// remove the dialog from history if already closed
|
||||
mainWindow.history.back();
|
||||
}
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
@ -151,6 +155,46 @@ export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// called on back()
|
||||
export const closeLastDialog = async () => {
|
||||
if (OPEN_DIALOG_STACK.length) {
|
||||
const lastDialog = OPEN_DIALOG_STACK.pop();
|
||||
const closed = await closeDialog(lastDialog!.dialogTag);
|
||||
if (!closed) {
|
||||
// if the dialog was not closed, put it back on the stack
|
||||
OPEN_DIALOG_STACK.push(lastDialog!);
|
||||
}
|
||||
if (OPEN_DIALOG_STACK.length && mainWindow.history.state?.opensDialog) {
|
||||
// if there are more dialogs open, push a new state so back() will close the next top dialog
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _handleClosed = async (ev: HASSDomEvent<DialogClosedParams>) => {
|
||||
// If not closed by navigating back, remove the open state from history
|
||||
const dialogIndex = OPEN_DIALOG_STACK.findIndex(
|
||||
(state) => state.dialogTag === ev.detail.dialog
|
||||
);
|
||||
if (dialogIndex !== -1) {
|
||||
OPEN_DIALOG_STACK.splice(dialogIndex, 1);
|
||||
}
|
||||
if (mainWindow.history.state?.dialog === ev.detail.dialog) {
|
||||
if (OPEN_DIALOG_STACK.length) {
|
||||
// if there are more dialogs open, set the top one in history
|
||||
mainWindow.history.replaceState(
|
||||
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },
|
||||
""
|
||||
);
|
||||
} else {
|
||||
mainWindow.history.back();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const makeDialogManager = (
|
||||
element: HTMLElement & ProvideHassElement,
|
||||
root: ShadowRoot | HTMLElement
|
||||
|
@ -1,22 +1,15 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { PropertyValueMap, ReactiveElement } from "lit";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type {
|
||||
DialogClosedParams,
|
||||
DialogState,
|
||||
import {
|
||||
closeLastDialog,
|
||||
showDialogFromHistory,
|
||||
} from "../dialogs/make-dialog-manager";
|
||||
import { closeDialog, showDialog } from "../dialogs/make-dialog-manager";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let historyPromise: Promise<void> | undefined;
|
||||
|
||||
let historyResolve: undefined | (() => void);
|
||||
|
||||
export const urlSyncMixin = <
|
||||
T extends Constructor<ReactiveElement & ProvideHassElement>,
|
||||
>(
|
||||
@ -26,8 +19,6 @@ export const urlSyncMixin = <
|
||||
__DEMO__
|
||||
? superClass
|
||||
: class extends superClass {
|
||||
private _ignoreNextPopState = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (mainWindow.history.length === 1) {
|
||||
@ -37,7 +28,6 @@ export const urlSyncMixin = <
|
||||
);
|
||||
}
|
||||
mainWindow.addEventListener("popstate", this._popstateChangeListener);
|
||||
this.addEventListener("dialog-closed", this._dialogClosedListener);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@ -46,7 +36,6 @@ export const urlSyncMixin = <
|
||||
"popstate",
|
||||
this._popstateChangeListener
|
||||
);
|
||||
this.removeEventListener("dialog-closed", this._dialogClosedListener);
|
||||
}
|
||||
|
||||
protected firstUpdated(
|
||||
@ -54,123 +43,25 @@ export const urlSyncMixin = <
|
||||
): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (mainWindow.history.state?.dialog) {
|
||||
this._handleDialogStateChange(mainWindow.history.state);
|
||||
showDialogFromHistory(mainWindow.history.state.dialog);
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogClosedListener = (
|
||||
ev: HASSDomEvent<DialogClosedParams>
|
||||
) => {
|
||||
if (DEBUG) {
|
||||
console.log("dialog closed", ev.detail.dialog);
|
||||
console.log(
|
||||
"open",
|
||||
mainWindow.history.state?.open,
|
||||
"dialog",
|
||||
mainWindow.history.state?.dialog
|
||||
);
|
||||
}
|
||||
// If not closed by navigating back, and not a new dialog is open, remove the open state from history
|
||||
if (
|
||||
mainWindow.history.state?.open &&
|
||||
mainWindow.history.state?.dialog === ev.detail.dialog
|
||||
) {
|
||||
if (DEBUG) {
|
||||
console.log("remove state", ev.detail.dialog);
|
||||
}
|
||||
if (mainWindow.history.length) {
|
||||
this._ignoreNextPopState = true;
|
||||
historyPromise = new Promise((resolve) => {
|
||||
historyResolve = () => {
|
||||
resolve();
|
||||
historyResolve = undefined;
|
||||
historyPromise = undefined;
|
||||
};
|
||||
mainWindow.history.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _popstateChangeListener = (ev: PopStateEvent) => {
|
||||
if (this._ignoreNextPopState) {
|
||||
if (
|
||||
history.length &&
|
||||
(ev.state?.oldState?.replaced ||
|
||||
ev.state?.oldState?.dialogParams === null)
|
||||
) {
|
||||
// if the previous dialog was replaced, or we could not copy the params, and the current dialog is closed, we should also remove the previous dialog from history
|
||||
if (DEBUG) {
|
||||
console.log("remove old state", ev.state.oldState);
|
||||
}
|
||||
mainWindow.history.back();
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log("ignore popstate");
|
||||
}
|
||||
this._ignoreNextPopState = false;
|
||||
if (historyResolve) {
|
||||
historyResolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.state && "dialog" in ev.state) {
|
||||
if (ev.state) {
|
||||
if (DEBUG) {
|
||||
console.log("popstate", ev);
|
||||
}
|
||||
this._handleDialogStateChange(ev.state);
|
||||
}
|
||||
if (historyResolve) {
|
||||
historyResolve();
|
||||
if (ev.state.opensDialog) {
|
||||
// coming back from a dialog
|
||||
// if we are instead navigating forward, the dialogs are already closed
|
||||
closeLastDialog();
|
||||
}
|
||||
if ("dialog" in ev.state) {
|
||||
// coming to a dialog
|
||||
// in practice the dialog stack is empty when navigating forward, so this is a no-op
|
||||
showDialogFromHistory(ev.state.dialog);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _handleDialogStateChange(state: DialogState) {
|
||||
if (DEBUG) {
|
||||
console.log("handle state", state);
|
||||
}
|
||||
if (!state.open) {
|
||||
const closed = await closeDialog(state.dialog);
|
||||
if (!closed) {
|
||||
if (DEBUG) {
|
||||
console.log("dialog could not be closed");
|
||||
}
|
||||
// dialog could not be closed, push state again
|
||||
mainWindow.history.pushState(
|
||||
{
|
||||
dialog: state.dialog,
|
||||
open: true,
|
||||
dialogParams: null,
|
||||
oldState: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.oldState) {
|
||||
if (DEBUG) {
|
||||
console.log("handle old state");
|
||||
}
|
||||
this._handleDialogStateChange(state.oldState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let shown = false;
|
||||
if (state.open && state.dialogParams !== null) {
|
||||
shown = await showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
state.dialog,
|
||||
state.dialogParams
|
||||
);
|
||||
}
|
||||
if (!shown) {
|
||||
// can't open dialog, update state
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, open: false },
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user