Skip to content

Commit 00868b2

Browse files
timmo001wendevlin
andauthored
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 <[email protected]> * Fix lint --------- Co-authored-by: Wendelin <[email protected]>
1 parent e573a72 commit 00868b2

File tree

2 files changed

+115
-66
lines changed

2 files changed

+115
-66
lines changed

src/dialogs/more-info/ha-more-info-dialog.ts

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import type { HassEntity } from "home-assistant-js-websocket";
1515
import type { PropertyValues } from "lit";
1616
import { LitElement, css, html, nothing } from "lit";
17-
import { customElement, property, state } from "lit/decorators";
17+
import { customElement, property, query, state } from "lit/decorators";
1818
import { cache } from "lit/directives/cache";
1919
import { keyed } from "lit/directives/keyed";
2020
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
@@ -50,7 +50,12 @@ import { lightSupportsFavoriteColors } from "../../data/light";
5050
import type { ItemType } from "../../data/search";
5151
import { SearchableDomains } from "../../data/search";
5252
import { getSensorNumericDeviceClasses } from "../../data/sensor";
53-
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
53+
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
54+
import {
55+
haStyleDialog,
56+
haStyleDialogFixedTop,
57+
haStyleScrollbar,
58+
} from "../../resources/styles";
5459
import "../../state-summary/state-card-content";
5560
import type { HomeAssistant } from "../../types";
5661
import {
@@ -96,13 +101,15 @@ declare global {
96101
const DEFAULT_VIEW: View = "info";
97102

98103
@customElement("ha-more-info-dialog")
99-
export class MoreInfoDialog extends LitElement {
104+
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
100105
@property({ attribute: false }) public hass!: HomeAssistant;
101106

102107
@property({ type: Boolean, reflect: true }) public large = false;
103108

104109
@state() private _parentEntityIds: string[] = [];
105110

111+
@query(".content") private _contentElement?: HTMLDivElement;
112+
106113
@state() private _entityId?: string | null;
107114

108115
@state() private _data?: Record<string, any>;
@@ -121,6 +128,12 @@ export class MoreInfoDialog extends LitElement {
121128

122129
@state() private _sensorNumericDeviceClasses?: string[] = [];
123130

131+
protected scrollFadeThreshold = 24;
132+
133+
protected get scrollableElement(): HTMLElement | null {
134+
return this._contentElement || null;
135+
}
136+
124137
public showDialog(params: MoreInfoDialogParams) {
125138
this._entityId = params.entityId;
126139
if (!this._entityId) {
@@ -594,78 +607,81 @@ export class MoreInfoDialog extends LitElement {
594607
`
595608
: nothing}
596609
</ha-dialog-header>
597-
${keyed(
598-
this._entityId,
599-
html`
600-
<div
601-
class="content"
602-
tabindex="-1"
603-
dialogInitialFocus
604-
@show-child-view=${this._showChildView}
605-
@entity-entry-updated=${this._entryUpdated}
606-
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
607-
@hass-more-info=${this._handleMoreInfoEvent}
608-
>
609-
${cache(
610-
this._childView
611-
? html`
612-
<div class="child-view">
613-
${dynamicElement(this._childView.viewTag, {
614-
hass: this.hass,
615-
entry: this._entry,
616-
params: this._childView.viewParams,
617-
})}
618-
</div>
619-
`
620-
: this._currView === "info"
610+
<div class="content-wrapper">
611+
${keyed(
612+
this._entityId,
613+
html`
614+
<div
615+
class="content ha-scrollbar"
616+
tabindex="-1"
617+
dialogInitialFocus
618+
@show-child-view=${this._showChildView}
619+
@entity-entry-updated=${this._entryUpdated}
620+
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
621+
@hass-more-info=${this._handleMoreInfoEvent}
622+
>
623+
${cache(
624+
this._childView
621625
? html`
622-
<ha-more-info-info
623-
dialogInitialFocus
624-
.hass=${this.hass}
625-
.entityId=${this._entityId}
626-
.entry=${this._entry}
627-
.editMode=${this._infoEditMode}
628-
.data=${this._data}
629-
></ha-more-info-info>
626+
<div class="child-view">
627+
${dynamicElement(this._childView.viewTag, {
628+
hass: this.hass,
629+
entry: this._entry,
630+
params: this._childView.viewParams,
631+
})}
632+
</div>
630633
`
631-
: this._currView === "history"
634+
: this._currView === "info"
632635
? html`
633-
<ha-more-info-history-and-logbook
636+
<ha-more-info-info
637+
dialogInitialFocus
634638
.hass=${this.hass}
635639
.entityId=${this._entityId}
636-
></ha-more-info-history-and-logbook>
640+
.entry=${this._entry}
641+
.editMode=${this._infoEditMode}
642+
.data=${this._data}
643+
></ha-more-info-info>
637644
`
638-
: this._currView === "settings"
645+
: this._currView === "history"
639646
? html`
640-
<ha-more-info-settings
647+
<ha-more-info-history-and-logbook
641648
.hass=${this.hass}
642649
.entityId=${this._entityId}
643-
.entry=${this._entry}
644-
></ha-more-info-settings>
650+
></ha-more-info-history-and-logbook>
645651
`
646-
: this._currView === "related"
652+
: this._currView === "settings"
647653
? html`
648-
<ha-related-items
654+
<ha-more-info-settings
649655
.hass=${this.hass}
650-
.itemId=${entityId}
651-
.itemType=${SearchableDomains.has(domain)
652-
? (domain as ItemType)
653-
: "entity"}
654-
></ha-related-items>
656+
.entityId=${this._entityId}
657+
.entry=${this._entry}
658+
></ha-more-info-settings>
655659
`
656-
: this._currView === "add_to"
660+
: this._currView === "related"
657661
? html`
658-
<ha-more-info-add-to
662+
<ha-related-items
659663
.hass=${this.hass}
660-
.entityId=${entityId}
661-
@add-to-action-selected=${this._goBack}
662-
></ha-more-info-add-to>
664+
.itemId=${entityId}
665+
.itemType=${SearchableDomains.has(domain)
666+
? (domain as ItemType)
667+
: "entity"}
668+
></ha-related-items>
663669
`
664-
: nothing
665-
)}
666-
</div>
667-
`
668-
)}
670+
: this._currView === "add_to"
671+
? html`
672+
<ha-more-info-add-to
673+
.hass=${this.hass}
674+
.entityId=${entityId}
675+
@add-to-action-selected=${this._goBack}
676+
></ha-more-info-add-to>
677+
`
678+
: nothing
679+
)}
680+
</div>
681+
`
682+
)}
683+
${this.renderScrollableFades()}
684+
</div>
669685
</ha-dialog>
670686
`;
671687
}
@@ -720,18 +736,27 @@ export class MoreInfoDialog extends LitElement {
720736

721737
static get styles() {
722738
return [
739+
...super.styles,
723740
haStyleDialog,
724741
haStyleDialogFixedTop,
742+
haStyleScrollbar,
725743
css`
726744
ha-dialog {
727745
--dialog-content-padding: 0;
728746
}
729747
730-
.content {
748+
.content-wrapper {
749+
flex: 1 1 auto;
750+
min-height: 0;
751+
position: relative;
731752
display: flex;
732753
flex-direction: column;
754+
}
755+
756+
.content {
733757
outline: none;
734758
flex: 1;
759+
overflow: auto;
735760
}
736761
737762
.child-view {

src/mixins/scrollable-fade-mixin.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ import type { Constructor } from "../types";
1313
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
1414
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
1515

16+
/**
17+
* Mixin that adds top and bottom fade overlays for scrollable content.
18+
* @param superClass - The LitElement class to extend.
19+
* @returns Extended class with scrollable fade functionality.
20+
*/
1621
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
1722
superClass: T
1823
) => {
1924
class ScrollableFadeClass extends superClass {
25+
/** Whether content has scrolled past the threshold. Controls top fade visibility. */
2026
@state() protected _contentScrolled = false;
2127

28+
/** Whether content extends beyond the viewport. Controls bottom fade visibility. */
2229
@state() protected _contentScrollable = false;
2330

2431
private _scrollTarget?: HTMLElement | null;
2532

2633
private _onScroll = (ev: Event) => {
2734
const target = ev.currentTarget as HTMLElement;
28-
this._contentScrolled = (target.scrollTop ?? 0) > 0;
35+
this._contentScrolled =
36+
(target.scrollTop ?? 0) > this.scrollFadeThreshold;
2937
this._updateScrollableState(target);
3038
};
3139

@@ -39,15 +47,26 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
3947
},
4048
});
4149

42-
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
50+
/**
51+
* Safe area padding in pixels for the scrollable element.
52+
*/
53+
protected scrollFadeSafeAreaPadding = 16;
4354

55+
/**
56+
* Scroll threshold in pixels for showing the fades.
57+
*/
58+
protected scrollFadeThreshold = 4;
59+
60+
/**
61+
* Default scrollable element value.
62+
*/
4463
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
4564
null;
4665

47-
protected get scrollFadeSafeAreaPadding() {
48-
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
49-
}
50-
66+
/**
67+
* Element to observe for scroll and resize events. Override with a getter to specify target.
68+
* Kept as a getter to allow subclasses to return query results.
69+
*/
5170
protected get scrollableElement(): HTMLElement | null {
5271
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
5372
}
@@ -67,6 +86,11 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
6786
super.disconnectedCallback();
6887
}
6988

89+
/**
90+
* Renders top and bottom fade overlays. Call in render method.
91+
* @param rounded - Whether to apply rounded corners.
92+
* @returns Template containing fade elements.
93+
*/
7094
protected renderScrollableFades(rounded = false): TemplateResult {
7195
return html`
7296
<div

0 commit comments

Comments
 (0)