mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-24 12:49:19 +00:00
Add scrollable fade to more info dialog (#28314)
* Add scrollable fade to more info dialog * Introduce scroll threshold (default 4px, more info 24px) * Docstrings * Remove getter for numeric values in mixin * Update src/dialogs/more-info/ha-more-info-dialog.ts Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> * Fix lint --------- Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
@@ -50,7 +50,12 @@ import { lightSupportsFavoriteColors } from "../../data/light";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
} from "../../resources/styles";
|
||||
import "../../state-summary/state-card-content";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -96,13 +101,15 @@ declare global {
|
||||
const DEFAULT_VIEW: View = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends LitElement {
|
||||
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _parentEntityIds: string[] = [];
|
||||
|
||||
@query(".content") private _contentElement?: HTMLDivElement;
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _data?: Record<string, any>;
|
||||
@@ -121,6 +128,12 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _sensorNumericDeviceClasses?: string[] = [];
|
||||
|
||||
protected scrollFadeThreshold = 24;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._contentElement || null;
|
||||
}
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
this._entityId = params.entityId;
|
||||
if (!this._entityId) {
|
||||
@@ -594,78 +607,81 @@ export class MoreInfoDialog extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
${keyed(
|
||||
this._entityId,
|
||||
html`
|
||||
<div
|
||||
class="content"
|
||||
tabindex="-1"
|
||||
dialogInitialFocus
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
<div class="content-wrapper">
|
||||
${keyed(
|
||||
this._entityId,
|
||||
html`
|
||||
<div
|
||||
class="content ha-scrollbar"
|
||||
tabindex="-1"
|
||||
dialogInitialFocus
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
.data=${this._data}
|
||||
></ha-more-info-info>
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "history"
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
.data=${this._data}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "settings"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
`
|
||||
: this._currView === "add_to"
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -720,18 +736,27 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
.content-wrapper {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
outline: none;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.child-view {
|
||||
|
||||
@@ -13,19 +13,27 @@ import type { Constructor } from "../types";
|
||||
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
|
||||
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
|
||||
|
||||
/**
|
||||
* Mixin that adds top and bottom fade overlays for scrollable content.
|
||||
* @param superClass - The LitElement class to extend.
|
||||
* @returns Extended class with scrollable fade functionality.
|
||||
*/
|
||||
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class ScrollableFadeClass extends superClass {
|
||||
/** Whether content has scrolled past the threshold. Controls top fade visibility. */
|
||||
@state() protected _contentScrolled = false;
|
||||
|
||||
/** Whether content extends beyond the viewport. Controls bottom fade visibility. */
|
||||
@state() protected _contentScrollable = false;
|
||||
|
||||
private _scrollTarget?: HTMLElement | null;
|
||||
|
||||
private _onScroll = (ev: Event) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
this._contentScrolled = (target.scrollTop ?? 0) > 0;
|
||||
this._contentScrolled =
|
||||
(target.scrollTop ?? 0) > this.scrollFadeThreshold;
|
||||
this._updateScrollableState(target);
|
||||
};
|
||||
|
||||
@@ -39,15 +47,26 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
},
|
||||
});
|
||||
|
||||
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
|
||||
/**
|
||||
* Safe area padding in pixels for the scrollable element.
|
||||
*/
|
||||
protected scrollFadeSafeAreaPadding = 16;
|
||||
|
||||
/**
|
||||
* Scroll threshold in pixels for showing the fades.
|
||||
*/
|
||||
protected scrollFadeThreshold = 4;
|
||||
|
||||
/**
|
||||
* Default scrollable element value.
|
||||
*/
|
||||
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
|
||||
null;
|
||||
|
||||
protected get scrollFadeSafeAreaPadding() {
|
||||
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element to observe for scroll and resize events. Override with a getter to specify target.
|
||||
* Kept as a getter to allow subclasses to return query results.
|
||||
*/
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
|
||||
}
|
||||
@@ -67,6 +86,11 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders top and bottom fade overlays. Call in render method.
|
||||
* @param rounded - Whether to apply rounded corners.
|
||||
* @returns Template containing fade elements.
|
||||
*/
|
||||
protected renderScrollableFades(rounded = false): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user