fix: Keep the notification popup visible / centered (#2702)
- Closes: #2693
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
} from '../../utils/action.js';
|
||||
import { arrayify } from '../../utils/basic.js';
|
||||
|
||||
const DISMISSING_INTERACTIONS = ['tap', 'hold', 'double_tap'];
|
||||
|
||||
export function handleControlAction(
|
||||
ev: CustomEvent<{ action: string }>,
|
||||
control: NotificationControl,
|
||||
@@ -17,7 +19,11 @@ export function handleControlAction(
|
||||
if (action) {
|
||||
dispatchActionExecutionRequest(host, { actions: arrayify(action) });
|
||||
}
|
||||
if (onDismiss && control.dismiss !== false) {
|
||||
if (
|
||||
onDismiss &&
|
||||
control.dismiss !== false &&
|
||||
DISMISSING_INTERACTIONS.includes(ev.detail.action)
|
||||
) {
|
||||
onDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
|
||||
import { getShadowRootHost } from '../../utils/shadow-root.js';
|
||||
|
||||
const INSET_TOP_PROPERTY = '--notification-popup-inset-top';
|
||||
const INSET_BOTTOM_PROPERTY = '--notification-popup-inset-bottom';
|
||||
|
||||
// Ensure the popup stays within the visible part of the card.
|
||||
export class NotificationPopupViewportController implements ReactiveController {
|
||||
private _host: ReactiveControllerHost & HTMLElement;
|
||||
private _resizeObserver = new ResizeObserver(() => this._update());
|
||||
|
||||
constructor(host: ReactiveControllerHost & HTMLElement) {
|
||||
this._host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
public hostConnected(): void {
|
||||
const container = this._getContainer();
|
||||
if (container) {
|
||||
this._resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
// Scroll events do not bubble, so the listener uses the capture phase to
|
||||
// observe scrolling in any element between the window and the container. A
|
||||
// Home Assistant dashboard scrolls an element within the page rather than
|
||||
// the window itself.
|
||||
window.addEventListener('scroll', this._update, { capture: true, passive: true });
|
||||
window.addEventListener('resize', this._update);
|
||||
|
||||
this._update();
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
this._resizeObserver.disconnect();
|
||||
window.removeEventListener('scroll', this._update, { capture: true });
|
||||
window.removeEventListener('resize', this._update);
|
||||
}
|
||||
|
||||
private _getContainer(): Element | null {
|
||||
return getShadowRootHost(this._host);
|
||||
}
|
||||
|
||||
private _update = (): void => {
|
||||
const container = this._getContainer();
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerBox = container.getBoundingClientRect();
|
||||
const visibleTop = Math.max(containerBox.top, 0);
|
||||
const visibleBottom = Math.min(containerBox.bottom, window.innerHeight);
|
||||
|
||||
if (visibleBottom <= visibleTop) {
|
||||
// No inset can bring the popup on screen when the container itself is
|
||||
// off screen, so the controller keeps the previous inset.
|
||||
return;
|
||||
}
|
||||
|
||||
this._host.style.setProperty(
|
||||
INSET_TOP_PROPERTY,
|
||||
`${visibleTop - containerBox.top}px`,
|
||||
);
|
||||
this._host.style.setProperty(
|
||||
INSET_BOTTOM_PROPERTY,
|
||||
`${containerBox.bottom - visibleBottom}px`,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||
import { handleControlAction } from '../../components-lib/notification/action.js';
|
||||
import { NotificationContextController } from '../../components-lib/notification/notification-context-controller.js';
|
||||
import { NotificationPopupController } from '../../components-lib/notification/notification-popup-controller.js';
|
||||
import { NotificationPopupViewportController } from '../../components-lib/notification/notification-popup-viewport-controller.js';
|
||||
import type { Notification } from '../../config/schema/actions/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import notificationPopupStyle from '../../scss/notification-popup.scss?inline';
|
||||
@@ -32,6 +33,13 @@ export class AdvancedCameraCardNotification extends LitElement {
|
||||
);
|
||||
private _contextController = new NotificationContextController(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Controller automatically registers itself with this element.
|
||||
new NotificationPopupViewportController(this);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.notification) {
|
||||
return;
|
||||
|
||||
+3
-6
@@ -1,11 +1,8 @@
|
||||
import { getShadowRootHost } from '../utils/shadow-root';
|
||||
|
||||
/**
|
||||
* Determine if a card is in panel mode.
|
||||
*/
|
||||
export const isCardInPanel = (card: HTMLElement): boolean => {
|
||||
const parent = card.getRootNode();
|
||||
return !!(
|
||||
parent &&
|
||||
parent instanceof ShadowRoot &&
|
||||
parent.host.tagName === 'HUI-PANEL-VIEW'
|
||||
);
|
||||
return getShadowRootHost(card)?.tagName === 'HUI-PANEL-VIEW';
|
||||
};
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
|
||||
:host {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
inset: var(--notification-popup-inset-top, 0) 0
|
||||
var(--notification-popup-inset-bottom, 0) 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
pointer-events: none;
|
||||
z-index: $z-index-notification;
|
||||
@@ -28,6 +31,12 @@
|
||||
|
||||
width: min(92%, 400px);
|
||||
max-height: calc(100% - 48px);
|
||||
|
||||
// min-height has priority over max-height: with the 40px vertical padding it
|
||||
// holds the border box at 48px (matched to equal control's 8px offset + 32px
|
||||
// + 8px).
|
||||
min-height: 8px;
|
||||
|
||||
padding: 20px 24px;
|
||||
padding-right: 48px;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Get the host of the shadow root an element lives in, or `null` when the
|
||||
* element is not in a shadow tree (in the light DOM, or not connected).
|
||||
*/
|
||||
export const getShadowRootHost = (element: Node): Element | null => {
|
||||
const root = element.getRootNode();
|
||||
return root instanceof ShadowRoot ? root.host : null;
|
||||
};
|
||||
Reference in New Issue
Block a user